1 Commits

Author SHA1 Message Date
dependabot[bot]
b8c8110f91 Bump nodemailer from 7.0.6 to 7.0.7
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.6 to 7.0.7.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.6...v7.0.7)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-07 19:28:27 +00:00
14 changed files with 57 additions and 149 deletions

View File

@@ -1,13 +0,0 @@
DATABASE_URL="..."
AUTH_SECRET="shhh_no_one_can_know"
AUTH_GOOGLE_ID="..."
AUTH_GOOGLE_SECRET="..."
STUDENT_URL="http://localhost:9000"
ADMIN_URL="http://localhost:9000"
ADMIN_DOMAIN="http://localhost:9001"
ALLOWED_EMAIL_DOMAIN="@somaiya.edu"
AUTH_TRUST_HOST=TRUE

View File

@@ -1,28 +1,31 @@
# NextPlacement # shadcn/ui monorepo template
NextPlacement is a placement-management platform built as a monorepo (pnpm workspaces + Turborepo) with multiple apps and shared packages. This template is for creating a monorepo with shadcn/ui.
## Repository Layout ## Usage
- `apps/` - application(s) (e.g., student/admin web apps)
- `packages/` - reusable packages/libraries used by apps
- `shared/` - sared code/assets (project-specific)
- `docker-compose.yml` / `docker-compose.dev.yml` - Docker compose configurations
- `DOCKER.md` - Docker notes / commands
## Prerequisites
- Git
- Node.js (LTS recommended)
- pnpm
- Docker Desktop (recommended for easiest setup)
## Quick Start (Docker)
1. Create environment file:
- Create `.env` in the project root (or copy from `.env.example` if present)
2.Start containers:
```bash ```bash
docker-compose up --build pnpm dlx shadcn@latest init
```
## Adding components
To add components to your app, run the following command at the root of your `web` app:
```bash
pnpm dlx shadcn@latest add button -c apps/web
```
This will place the ui components in the `packages/ui/src/components` directory.
## Tailwind
Your `tailwind.config.ts` and `globals.css` are already set up to use the components from the `ui` package.
## Using components
To use the components in your app, import them from the `ui` package.
```tsx
import { Button } from '@workspace/ui/components/button';
```

View File

@@ -1,2 +0,0 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nextplacement
AUTH_SECRET=change_me

View File

@@ -1,6 +1,4 @@
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Alert, AlertDescription, AlertTitle } from '@workspace/ui/components/alert';
import { AlertCircle } from 'lucide-react';
import { signIn } from '@/auth'; import { signIn } from '@/auth';
async function logIn() { async function logIn() {
@@ -8,9 +6,7 @@ async function logIn() {
await signIn('google', { redirectTo: '/' }); await signIn('google', { redirectTo: '/' });
} }
export default async function Page(props: { searchParams: Promise<{ error?: string }> }) { export default async function Page() {
const searchParams = await props.searchParams;
const error = searchParams?.error;
return ( return (
<div className="relative min-h-svh flex items-center justify-center overflow-hidden bg-gradient-to-br from-blue-100 via-red-100 to-pink-100 transition-colors duration-500"> <div className="relative min-h-svh flex items-center justify-center overflow-hidden bg-gradient-to-br from-blue-100 via-red-100 to-pink-100 transition-colors duration-500">
{/* Animated floating shapes */} {/* Animated floating shapes */}
@@ -22,22 +18,11 @@ export default async function Page(props: { searchParams: Promise<{ error?: stri
<div className="relative z-10 backdrop-blur-md bg-white/70 rounded-2xl shadow-2xl p-10 flex flex-col items-center gap-8 border border-white/30 max-w-sm w-full transition-all duration-300 hover:shadow-[0_0_32px_4px_rgba(239,68,68,0.25)] hover:border-red-400/60"> <div className="relative z-10 backdrop-blur-md bg-white/70 rounded-2xl shadow-2xl p-10 flex flex-col items-center gap-8 border border-white/30 max-w-sm w-full transition-all duration-300 hover:shadow-[0_0_32px_4px_rgba(239,68,68,0.25)] hover:border-red-400/60">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
{/* Animated logo */} {/* Animated logo */}
<img src="/favicon.ico" alt="Logo" className="w-14 h-14 mb-2 drop-shadow-lg animate-bounce-slow" /> <img src="favicon.ico" alt="Logo" className="w-14 h-14 mb-2 drop-shadow-lg animate-bounce-slow" />
<h1 className="text-2xl font-bold text-gray-800 tracking-tight">Placement Portal Admin</h1> <h1 className="text-2xl font-bold text-gray-800 tracking-tight">Placement Portal Admin</h1>
<p className="text-gray-500 text-sm text-center">Sign in to manage placements and students</p> <p className="text-gray-500 text-sm text-center">Sign in to manage placements and students</p>
<p className="text-xs text-red-500 font-semibold italic mt-1 animate-fade-in">Empower your journey. Shape the future.</p> <p className="text-xs text-red-500 font-semibold italic mt-1 animate-fade-in">Empower your journey. Shape the future.</p>
</div> </div>
{error === 'AccessDenied' && (
<Alert variant="destructive" className="animate-in fade-in slide-in-from-top-4 duration-300">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Access Denied</AlertTitle>
<AlertDescription>
Please sign in with your somaiya email ID.
</AlertDescription>
</Alert>
)}
<form action={logIn} className="w-full"> <form action={logIn} className="w-full">
<Button type="submit" variant="outline" className="w-full h-12 relative overflow-hidden group rounded-lg shadow-md hover:shadow-lg transition-all focus:ring-2 focus:ring-red-400"> <Button type="submit" variant="outline" className="w-full h-12 relative overflow-hidden group rounded-lg shadow-md hover:shadow-lg transition-all focus:ring-2 focus:ring-red-400">
<span className="absolute inset-0 bg-gradient-to-r from-red-200/0 via-red-200/20 to-red-200/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" /> <span className="absolute inset-0 bg-gradient-to-r from-red-200/0 via-red-200/20 to-red-200/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" />

View File

@@ -1,6 +1,6 @@
import NextAuth, { type DefaultSession } from 'next-auth'; import NextAuth, { type DefaultSession } from 'next-auth';
import type { NextAuthConfig } from 'next-auth'; import type { NextAuthConfig } from 'next-auth';
import Google from 'next-auth/providers/google'; import Google from "next-auth/providers/google";
import { db, admins, students } from '@workspace/db'; import { db, admins, students } from '@workspace/db';
import { eq } from '@workspace/db/drizzle'; import { eq } from '@workspace/db/drizzle';
@@ -12,7 +12,7 @@ declare module 'next-auth' {
studentId?: number; studentId?: number;
completedProfile?: boolean; completedProfile?: boolean;
[key: string]: any; [key: string]: any;
} & DefaultSession['user']; } & DefaultSession["user"];
} }
interface JWT { interface JWT {
@@ -32,13 +32,6 @@ declare module 'next/server' {
const authConfig: NextAuthConfig = { const authConfig: NextAuthConfig = {
providers: [Google], providers: [Google],
callbacks: { callbacks: {
async signIn({ user }) {
const allowedDomain = process.env.ALLOWED_EMAIL_DOMAIN;
if (allowedDomain && user.email && !user.email.endsWith(allowedDomain)) {
return false;
}
return true;
},
async jwt({ token, account, user }) { async jwt({ token, account, user }) {
// Only check DB on first sign in // Only check DB on first sign in
if (account && user && user.email) { if (account && user && user.email) {

View File

@@ -5,8 +5,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "dotenv -e ../../.env -- next dev --turbopack -p 9001", "dev": "dotenv -e ../../.env -- next dev --turbopack -p 9001",
"build": "dotenv -e ../../.env -- next build && cp -r .next/static .next/standalone/apps/admin/.next/static && cp -r public .next/standalone/apps/admin/public", "build": "dotenv -e ../../.env -- next build",
"start": "PORT=9001 dotenv -e ../../.env -- node .next/standalone/apps/admin/server.js", "start": "dotenv -e ../../.env -- next start -p 9001",
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@@ -1,3 +0,0 @@
ADMIN_DOMAIN=http://localhost:9001
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nextplacement
AUTH_SECRET=change_me

View File

@@ -1,6 +1,4 @@
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Alert, AlertDescription, AlertTitle } from '@workspace/ui/components/alert';
import { AlertCircle } from 'lucide-react';
import { signIn } from '@/auth'; import { signIn } from '@/auth';
async function logIn() { async function logIn() {
@@ -8,10 +6,7 @@ async function logIn() {
await signIn('google', { redirectTo: '/' }); await signIn('google', { redirectTo: '/' });
} }
// export default async function Page() { export default async function Page() {
export default async function Page(props: { searchParams: Promise<{ error?: string }> }) {
const searchParams = await props.searchParams;
const error = searchParams?.error;
return ( return (
<div className="relative min-h-svh flex items-center justify-center overflow-hidden bg-gradient-to-br from-blue-100 via-red-100 to-pink-100 transition-colors duration-500"> <div className="relative min-h-svh flex items-center justify-center overflow-hidden bg-gradient-to-br from-blue-100 via-red-100 to-pink-100 transition-colors duration-500">
{/* Animated floating shapes */} {/* Animated floating shapes */}
@@ -28,17 +23,6 @@ export default async function Page(props: { searchParams: Promise<{ error?: stri
<p className="text-gray-500 text-sm text-center">Sign in to manage your placements</p> <p className="text-gray-500 text-sm text-center">Sign in to manage your placements</p>
<p className="text-xs text-red-500 font-semibold italic mt-1 animate-fade-in">Empower your journey. Shape the future.</p> <p className="text-xs text-red-500 font-semibold italic mt-1 animate-fade-in">Empower your journey. Shape the future.</p>
</div> </div>
{error === 'AccessDenied' && (
<Alert variant="destructive" className="animate-in fade-in slide-in-from-top-4 duration-300">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Access Denied</AlertTitle>
<AlertDescription>
Please sign in with your somaiya email ID.
</AlertDescription>
</Alert>
)}
<form action={logIn} className="w-full"> <form action={logIn} className="w-full">
<Button type="submit" variant="outline" className="w-full h-12 relative overflow-hidden group rounded-lg shadow-md hover:shadow-lg transition-all focus:ring-2 focus:ring-red-400"> <Button type="submit" variant="outline" className="w-full h-12 relative overflow-hidden group rounded-lg shadow-md hover:shadow-lg transition-all focus:ring-2 focus:ring-red-400">
<span className="absolute inset-0 bg-gradient-to-r from-red-200/0 via-red-200/20 to-red-200/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" /> <span className="absolute inset-0 bg-gradient-to-r from-red-200/0 via-red-200/20 to-red-200/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" />

View File

@@ -1,6 +1,6 @@
import NextAuth, { type DefaultSession } from 'next-auth'; import NextAuth, { type DefaultSession } from 'next-auth';
import type { NextAuthConfig } from 'next-auth'; import type { NextAuthConfig } from 'next-auth';
import Google from 'next-auth/providers/google'; import Google from "next-auth/providers/google";
import { db, admins, students } from '@workspace/db'; import { db, admins, students } from '@workspace/db';
import { eq } from '@workspace/db/drizzle'; import { eq } from '@workspace/db/drizzle';
@@ -12,7 +12,7 @@ declare module 'next-auth' {
studentId?: number; studentId?: number;
completedProfile?: boolean; completedProfile?: boolean;
[key: string]: any; [key: string]: any;
} & DefaultSession['user']; } & DefaultSession["user"];
} }
interface JWT { interface JWT {
@@ -32,13 +32,6 @@ declare module 'next/server' {
const authConfig: NextAuthConfig = { const authConfig: NextAuthConfig = {
providers: [Google], providers: [Google],
callbacks: { callbacks: {
async signIn({ user }) {
const allowedDomain = process.env.ALLOWED_EMAIL_DOMAIN;
if (allowedDomain && user.email && !user.email.endsWith(allowedDomain)) {
return false;
}
return true;
},
async jwt({ token, account, user }) { async jwt({ token, account, user }) {
// Only set role, adminId, studentId, and email in JWT // Only set role, adminId, studentId, and email in JWT
const email = user?.email || token?.email; const email = user?.email || token?.email;
@@ -101,9 +94,6 @@ const authConfig: NextAuthConfig = {
return session; return session;
}, },
}, },
pages: {
error: '/login',
},
}; };
// Note: TypeScript warnings about inferred types are expected with NextAuth v5 beta // Note: TypeScript warnings about inferred types are expected with NextAuth v5 beta

View File

@@ -40,9 +40,6 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const deadline = new Date(job.applicationDeadline);
const isDeadlinePassed = new Date() > deadline;
const handleApply = async () => { const handleApply = async () => {
if (!selectedResume) { if (!selectedResume) {
setMessage({ type: 'error', text: 'Please select a resume' }); setMessage({ type: 'error', text: 'Please select a resume' });
@@ -66,14 +63,12 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
setMessage({ type: 'error', text: result.error || 'Failed to submit application' }); setMessage({ type: 'error', text: result.error || 'Failed to submit application' });
} }
} catch (error) { } catch (error) {
setMessage({ setMessage({ type: 'error', text: 'An error occurred while submitting your application' });
type: 'error',
text: error instanceof Error ? error.message : 'An error occurred while submitting your application'
});
} }
}); });
}; };
const isDeadlinePassed = new Date() > new Date(job.applicationDeadline as any);
const cannotApplyReason = isApplied const cannotApplyReason = isApplied
? 'You have already applied to this job' ? 'You have already applied to this job'
: resumes.length === 0 : resumes.length === 0
@@ -83,33 +78,21 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
: null; : null;
return ( return (
<Dialog open={isOpen} onOpenChange={(open) => !isPending && setIsOpen(open)}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
{!cannotApplyReason && ( <DialogTrigger asChild>
<DialogTrigger asChild>
<div className="flex flex-col items-start">
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-700"
>
Apply Now
</Button>
</div>
</DialogTrigger>
)}
{cannotApplyReason && (
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<Button <Button
size="sm" size="sm"
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
disabled disabled={Boolean(cannotApplyReason)}
> >
{isApplied ? 'Applied' : 'Apply Now'} {isApplied ? 'Applied' : 'Apply Now'}
</Button> </Button>
<span className="mt-1 text-xs text-red-600">{cannotApplyReason}</span> {cannotApplyReason && (
<span className="mt-1 text-xs text-red-600">{cannotApplyReason}</span>
)}
</div> </div>
)} </DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@@ -144,7 +127,7 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
<span>Deadline: {deadline.toLocaleDateString()}</span> <span>Deadline: {job.applicationDeadline.toLocaleDateString()}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Star className="w-4 h-4" /> <Star className="w-4 h-4" />
@@ -179,18 +162,6 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{selectedResume && (
<a
href={resumes.find(r => r.id.toString() === selectedResume)?.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 underline mt-2 inline-block"
>
Preview selected resume
</a>
)}
{resumes.length === 0 && ( {resumes.length === 0 && (
<p className="text-sm text-red-600 mt-1"> <p className="text-sm text-red-600 mt-1">
No resumes found. Please upload a resume first. No resumes found. Please upload a resume first.
@@ -200,10 +171,11 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
{/* Message Display */} {/* Message Display */}
{message && ( {message && (
<div className={`p-3 rounded-lg ${message.type === 'success' <div className={`p-3 rounded-lg ${
message.type === 'success'
? 'bg-green-100 text-green-700 border border-green-200' ? 'bg-green-100 text-green-700 border border-green-200'
: 'bg-red-100 text-red-700 border border-red-200' : 'bg-red-100 text-red-700 border border-red-200'
}`}> }`}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{message.type === 'success' ? ( {message.type === 'success' ? (
<CheckCircle className="w-4 h-4" /> <CheckCircle className="w-4 h-4" />

View File

@@ -5,8 +5,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "dotenv -e ../../.env -- next dev --turbopack -p 9000", "dev": "dotenv -e ../../.env -- next dev --turbopack -p 9000",
"build": "dotenv -e ../../.env -- next build && cp -r .next/static .next/standalone/apps/student/.next/static && cp -r public .next/standalone/apps/student/public", "build": "dotenv -e ../../.env -- next build",
"start": "PORT=9000 dotenv -e ../../.env -- node .next/standalone/apps/student/server.js", "start": "dotenv -e ../../.env -- next start -p 9000",
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

Submodule nextplacement deleted from 1648a56680

View File

@@ -34,6 +34,6 @@
] ]
}, },
"dependencies": { "dependencies": {
"nodemailer": "^7.0.6" "nodemailer": "^7.0.7"
} }
} }

10
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
nodemailer: nodemailer:
specifier: ^7.0.6 specifier: ^7.0.7
version: 7.0.6 version: 7.0.7
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^9.32.0 specifier: ^9.32.0
@@ -3269,8 +3269,8 @@ packages:
resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
nodemailer@7.0.6: nodemailer@7.0.7:
resolution: {integrity: sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==} resolution: {integrity: sha512-jGOaRznodf62TVzdyhKt/f1Q/c3kYynk8629sgJHpRzGZj01ezbgMMWJSAjHADcwTKxco3B68/R+KHJY2T5BaA==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
npm-run-path@4.0.1: npm-run-path@4.0.1:
@@ -7271,7 +7271,7 @@ snapshots:
nodemailer@6.10.1: {} nodemailer@6.10.1: {}
nodemailer@7.0.6: {} nodemailer@7.0.7: {}
npm-run-path@4.0.1: npm-run-path@4.0.1:
dependencies: dependencies: