forked from Arnab-Afk/nextplacement
Compare commits
11 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ad56c67d47 | |||
|
|
6cc3e4a086 | ||
| 91a3f95a0a | |||
| e3710b93fa | |||
| 28d1f78e18 | |||
|
|
3a1deddb8c | ||
|
|
441ae8169b | ||
|
|
3bcb5b851d | ||
| 7433052878 | |||
| b6fcc63c82 | |||
| 1648a56680 |
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
51
README.md
51
README.md
@@ -1,31 +1,28 @@
|
|||||||
# shadcn/ui monorepo template
|
# NextPlacement
|
||||||
|
|
||||||
This template is for creating a monorepo with shadcn/ui.
|
NextPlacement is a placement-management platform built as a monorepo (pnpm workspaces + Turborepo) with multiple apps and shared packages.
|
||||||
|
|
||||||
## Usage
|
## Repository Layout
|
||||||
|
- `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
|
||||||
pnpm dlx shadcn@latest init
|
docker-compose up --build
|
||||||
```
|
|
||||||
|
|
||||||
## 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';
|
|
||||||
```
|
|
||||||
|
|||||||
2
apps/admin/.env.example
Normal file
2
apps/admin/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nextplacement
|
||||||
|
AUTH_SECRET=change_me
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
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() {
|
||||||
@@ -6,7 +8,9 @@ async function logIn() {
|
|||||||
await signIn('google', { redirectTo: '/' });
|
await signIn('google', { redirectTo: '/' });
|
||||||
}
|
}
|
||||||
|
|
||||||
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 */}
|
||||||
@@ -18,11 +22,22 @@ export default async function Page() {
|
|||||||
<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" />
|
||||||
|
|||||||
@@ -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,6 +32,13 @@ 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) {
|
||||||
|
|||||||
@@ -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",
|
"build": "dotenv -e ../../.env -- next build && cp -r .next/static .next/standalone/apps/admin/.next/static && cp -r public .next/standalone/apps/admin/public",
|
||||||
"start": "dotenv -e ../../.env -- next start -p 9001",
|
"start": "PORT=9001 dotenv -e ../../.env -- node .next/standalone/apps/admin/server.js",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
|||||||
3
apps/student/.env.example
Normal file
3
apps/student/.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ADMIN_DOMAIN=http://localhost:9001
|
||||||
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nextplacement
|
||||||
|
AUTH_SECRET=change_me
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
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() {
|
||||||
@@ -6,7 +8,10 @@ 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 */}
|
||||||
@@ -23,6 +28,17 @@ export default async function Page() {
|
|||||||
<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" />
|
||||||
|
|||||||
@@ -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,6 +32,13 @@ 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;
|
||||||
@@ -94,6 +101,9 @@ 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
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ 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' });
|
||||||
@@ -63,12 +66,14 @@ 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({ type: 'error', text: 'An error occurred while submitting your application' });
|
setMessage({
|
||||||
|
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
|
||||||
@@ -78,21 +83,33 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={(open) => !isPending && setIsOpen(open)}>
|
||||||
|
{!cannotApplyReason && (
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<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={Boolean(cannotApplyReason)}
|
>
|
||||||
|
Apply Now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogTrigger>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cannotApplyReason && (
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
{isApplied ? 'Applied' : 'Apply Now'}
|
{isApplied ? 'Applied' : 'Apply Now'}
|
||||||
</Button>
|
</Button>
|
||||||
{cannotApplyReason && (
|
|
||||||
<span className="mt-1 text-xs text-red-600">{cannotApplyReason}</span>
|
<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">
|
||||||
@@ -127,7 +144,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: {job.applicationDeadline.toLocaleDateString()}</span>
|
<span>Deadline: {deadline.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" />
|
||||||
@@ -162,6 +179,18 @@ 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.
|
||||||
@@ -171,8 +200,7 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
|
|||||||
|
|
||||||
{/* Message Display */}
|
{/* Message Display */}
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`p-3 rounded-lg ${
|
<div className={`p-3 rounded-lg ${message.type === 'success'
|
||||||
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'
|
||||||
}`}>
|
}`}>
|
||||||
|
|||||||
@@ -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",
|
"build": "dotenv -e ../../.env -- next build && cp -r .next/static .next/standalone/apps/student/.next/static && cp -r public .next/standalone/apps/student/public",
|
||||||
"start": "dotenv -e ../../.env -- next start -p 9000",
|
"start": "PORT=9000 dotenv -e ../../.env -- node .next/standalone/apps/student/server.js",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
|||||||
1
nextplacement
Submodule
1
nextplacement
Submodule
Submodule nextplacement added at 1648a56680
@@ -34,6 +34,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nodemailer": "^7.0.7"
|
"nodemailer": "^7.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^7.0.7
|
specifier: ^7.0.6
|
||||||
version: 7.0.7
|
version: 7.0.6
|
||||||
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.7:
|
nodemailer@7.0.6:
|
||||||
resolution: {integrity: sha512-jGOaRznodf62TVzdyhKt/f1Q/c3kYynk8629sgJHpRzGZj01ezbgMMWJSAjHADcwTKxco3B68/R+KHJY2T5BaA==}
|
resolution: {integrity: sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==}
|
||||||
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.7: {}
|
nodemailer@7.0.6: {}
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user