Initial commit
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import DiscordProvider from "next-auth/providers/discord";
|
||||
|
||||
const ALLOWED_DISCORD_IDS =
|
||||
process.env.ALLOWED_DISCORD_IDS?.split(",").map((id) => id.trim()) || [];
|
||||
const ADMIN_DISCORD_IDS =
|
||||
process.env.ADMIN_DISCORD_IDS?.split(",").map((id) => id.trim()) || [];
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
DiscordProvider({
|
||||
clientId: process.env.DISCORD_CLIENT_ID!,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async signIn({ profile }) {
|
||||
if (profile?.id && ALLOWED_DISCORD_IDS.includes(profile.id)) {
|
||||
return true;
|
||||
}
|
||||
return "/unauthorized";
|
||||
},
|
||||
async jwt({ token, profile }) {
|
||||
if (profile) {
|
||||
token.discordId = profile.id;
|
||||
token.isAdmin = ADMIN_DISCORD_IDS.includes(
|
||||
profile.id as string
|
||||
);
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.discordId = token.discordId as string;
|
||||
session.user.isAdmin = token.isAdmin as boolean;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/unauthorized",
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -0,0 +1,171 @@
|
||||
import { initDatabase, query } from "@/lib/db";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authOptions } from "../auth/[...nextauth]/route";
|
||||
|
||||
async function isAdmin() {
|
||||
const session = await getServerSession(authOptions);
|
||||
return session?.user?.isAdmin === true;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await initDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mode = searchParams.get("mode");
|
||||
|
||||
let result;
|
||||
if (mode) {
|
||||
result = await query(
|
||||
"SELECT * FROM questions WHERE mode = $1 ORDER BY order_index ASC, id ASC",
|
||||
[mode]
|
||||
);
|
||||
} else {
|
||||
result = await query(
|
||||
"SELECT * FROM questions ORDER BY mode, order_index ASC, id ASC"
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error("Error fetching questions:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch questions" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin())) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
await initDatabase();
|
||||
const body = await request.json();
|
||||
const { category, question, mode } = body;
|
||||
|
||||
if (!category || !question || !mode) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const maxOrder = await query(
|
||||
"SELECT COALESCE(MAX(order_index), -1) as max_order FROM questions WHERE mode = $1",
|
||||
[mode]
|
||||
);
|
||||
const newOrder = parseInt(maxOrder.rows[0].max_order) + 1;
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO questions (category, question, mode, order_index) VALUES ($1, $2, $3, $4) RETURNING *",
|
||||
[category, question, mode, newOrder]
|
||||
);
|
||||
|
||||
return NextResponse.json(result.rows[0], { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating question:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create question" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin())) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { id, category, question, order_index } = body;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing question id" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const args: (string | number)[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (category !== undefined) {
|
||||
updates.push(`category = $${paramIndex++}`);
|
||||
args.push(category);
|
||||
}
|
||||
if (question !== undefined) {
|
||||
updates.push(`question = $${paramIndex++}`);
|
||||
args.push(question);
|
||||
}
|
||||
if (order_index !== undefined) {
|
||||
updates.push(`order_index = $${paramIndex++}`);
|
||||
args.push(order_index);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No fields to update" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
args.push(id);
|
||||
|
||||
const result = await query(
|
||||
`UPDATE questions SET ${updates.join(
|
||||
", "
|
||||
)} WHERE id = $${paramIndex} RETURNING *`,
|
||||
args
|
||||
);
|
||||
|
||||
return NextResponse.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error("Error updating question:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update question" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
if (!(await isAdmin())) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing question id" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await query("DELETE FROM questions WHERE id = $1", [id]);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting question:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete question" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { initDatabase, query } from "@/lib/db";
|
||||
import { SessionAnswer } from "@/types";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await initDatabase();
|
||||
const sessionsResult = await query(
|
||||
"SELECT * FROM sessions ORDER BY created_at DESC"
|
||||
);
|
||||
|
||||
const sessions = await Promise.all(
|
||||
sessionsResult.rows.map(async (session: any) => {
|
||||
const answersResult = await query(
|
||||
"SELECT * FROM session_answers WHERE session_id = $1 ORDER BY question_index",
|
||||
[session.id]
|
||||
);
|
||||
|
||||
return {
|
||||
id: session.id,
|
||||
mode: session.mode,
|
||||
created_at: session.created_at,
|
||||
exported_at: session.exported_at,
|
||||
answers: answersResult.rows.map((a: any) => ({
|
||||
questionIndex: a.question_index,
|
||||
category: a.category,
|
||||
question: a.question,
|
||||
answer: a.answer,
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json(sessions);
|
||||
} catch (error) {
|
||||
console.error("Error fetching sessions:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch sessions" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
await initDatabase();
|
||||
const body = await request.json();
|
||||
const { mode, answers } = body;
|
||||
|
||||
if (!mode || !answers) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const sessionResult = await query(
|
||||
"INSERT INTO sessions (mode) VALUES ($1) RETURNING *",
|
||||
[mode]
|
||||
);
|
||||
|
||||
const sessionId = sessionResult.rows[0].id;
|
||||
|
||||
for (const answer of answers as SessionAnswer[]) {
|
||||
if (answer.answer.trim()) {
|
||||
await query(
|
||||
"INSERT INTO session_answers (session_id, question_index, category, question, answer) VALUES ($1, $2, $3, $4, $5)",
|
||||
[
|
||||
sessionId,
|
||||
answer.questionIndex,
|
||||
answer.category,
|
||||
answer.question,
|
||||
answer.answer,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const answersData = await query(
|
||||
"SELECT * FROM session_answers WHERE session_id = $1 ORDER BY question_index",
|
||||
[sessionId]
|
||||
);
|
||||
|
||||
const newSession = {
|
||||
id: sessionResult.rows[0].id,
|
||||
mode: sessionResult.rows[0].mode,
|
||||
created_at: sessionResult.rows[0].created_at,
|
||||
answers: answersData.rows.map((a: any) => ({
|
||||
questionIndex: a.question_index,
|
||||
category: a.category,
|
||||
question: a.question,
|
||||
answer: a.answer,
|
||||
})),
|
||||
};
|
||||
|
||||
return NextResponse.json(newSession, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating session:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create session" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { initDatabase, query } from "@/lib/db";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await initDatabase();
|
||||
const result = await query(
|
||||
"SELECT * FROM tasks ORDER BY created_at DESC"
|
||||
);
|
||||
return NextResponse.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error("Error fetching tasks:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch tasks" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
await initDatabase();
|
||||
const body = await request.json();
|
||||
const { text, category, description } = body;
|
||||
|
||||
if (!text || !category) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const added = new Date().toLocaleDateString("fr-FR");
|
||||
|
||||
const result = await query(
|
||||
"INSERT INTO tasks (text, description, checklist, category, status, added) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *",
|
||||
[text, description || null, "[]", category, "todo", added]
|
||||
);
|
||||
|
||||
return NextResponse.json(result.rows[0], { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating task:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create task" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { id, status, category, text, description, checklist } = body;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing task id" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const args: (string | number | null)[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (status !== undefined) {
|
||||
updates.push(`status = $${paramIndex++}`);
|
||||
args.push(status);
|
||||
}
|
||||
if (category !== undefined) {
|
||||
updates.push(`category = $${paramIndex++}`);
|
||||
args.push(category);
|
||||
}
|
||||
if (text !== undefined) {
|
||||
updates.push(`text = $${paramIndex++}`);
|
||||
args.push(text);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updates.push(`description = $${paramIndex++}`);
|
||||
args.push(description);
|
||||
}
|
||||
if (checklist !== undefined) {
|
||||
updates.push(`checklist = $${paramIndex++}`);
|
||||
args.push(JSON.stringify(checklist));
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No fields to update" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
args.push(id);
|
||||
|
||||
const result = await query(
|
||||
`UPDATE tasks SET ${updates.join(
|
||||
", "
|
||||
)} WHERE id = $${paramIndex} RETURNING *`,
|
||||
args
|
||||
);
|
||||
|
||||
return NextResponse.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error("Error updating task:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update task" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing task id" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await query("DELETE FROM tasks WHERE id = $1", [id]);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting task:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete task" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: #ffffff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
textarea {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4b5563 #1f2937;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 669 B |
@@ -0,0 +1,22 @@
|
||||
import AuthProvider from "@/components/AuthProvider";
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Brain Dump",
|
||||
description: "Organise tes pensées avec Brain Dump",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<body className="antialiased bg-gray-900 text-white">
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [session, router]);
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-gray-800 rounded-2xl p-8 max-w-md w-full shadow-2xl border border-gray-700"
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", duration: 0.6 }}
|
||||
className="text-6xl mb-4"
|
||||
>
|
||||
🧠
|
||||
</motion.div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">
|
||||
Brain Dump
|
||||
</h1>
|
||||
<p className="text-gray-400">
|
||||
Connectez-vous pour accéder à l'application
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => signIn("discord", { callbackUrl: "/" })}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#5865F2] hover:bg-[#4752C4] text-white font-semibold py-4 px-6 rounded-xl transition-colors duration-200"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
Se connecter avec Discord
|
||||
</motion.button>
|
||||
|
||||
<p className="text-center text-gray-500 text-sm mt-6">
|
||||
Seuls les utilisateurs autorisés peuvent accéder à cette
|
||||
application.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", duration: 0.6 }}
|
||||
className="text-8xl mb-6"
|
||||
>
|
||||
🤔
|
||||
</motion.div>
|
||||
<h1 className="text-4xl font-bold text-white mb-4">404</h1>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Cette page n'existe pas
|
||||
</p>
|
||||
<Link href="/">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-indigo-500 hover:bg-indigo-600 text-white font-semibold py-3 px-8 rounded-xl transition-colors"
|
||||
>
|
||||
Retour à l'accueil
|
||||
</motion.button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
"use client";
|
||||
|
||||
import CompletePage from "@/components/CompletePage";
|
||||
import Confetti from "@/components/Confetti";
|
||||
import HomePage from "@/components/HomePage";
|
||||
import KanbanBoard from "@/components/KanbanBoard";
|
||||
import Questionnaire from "@/components/Questionnaire";
|
||||
import QuestionsDashboard from "@/components/QuestionsDashboard";
|
||||
import { CRISIS_QUESTIONS, NORMAL_QUESTIONS } from "@/lib/constants";
|
||||
import { Question, QuestionMode, SessionAnswer, TaskCategory } from "@/types";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Screen = "home" | "questions" | "complete" | "kanban" | "dashboard";
|
||||
|
||||
export default function Home() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [screen, setScreen] = useState<Screen>("home");
|
||||
const [mode, setMode] = useState<QuestionMode>("normal");
|
||||
const [sessionAnswers, setSessionAnswers] = useState<SessionAnswer[]>([]);
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const [dbQuestions, setDbQuestions] = useState<Question[]>([]);
|
||||
const [questionsLoaded, setQuestionsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/login");
|
||||
}
|
||||
}, [status, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== "authenticated") return;
|
||||
|
||||
const loadQuestions = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/questions");
|
||||
const data = await response.json();
|
||||
setDbQuestions(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading questions:", error);
|
||||
} finally {
|
||||
setQuestionsLoaded(true);
|
||||
}
|
||||
};
|
||||
loadQuestions();
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
if (screen === "questions") {
|
||||
if (confirm("Abandonner le questionnaire ?")) {
|
||||
setScreen("home");
|
||||
}
|
||||
} else if (screen !== "home") {
|
||||
setScreen("home");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [screen]);
|
||||
|
||||
const startQuestionnaire = (crisisMode: boolean) => {
|
||||
setMode(crisisMode ? "crisis" : "normal");
|
||||
setScreen("questions");
|
||||
};
|
||||
|
||||
const handleAddTask = async (text: string, category: TaskCategory) => {
|
||||
const response = await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text, category }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to add task");
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuestionnaireComplete = async (answers: SessionAnswer[]) => {
|
||||
setSessionAnswers(answers);
|
||||
|
||||
try {
|
||||
await fetch("/api/sessions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mode, answers }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving session:", error);
|
||||
}
|
||||
|
||||
setShowConfetti(true);
|
||||
setTimeout(() => setShowConfetti(false), 3000);
|
||||
setScreen("complete");
|
||||
};
|
||||
|
||||
const exportSession = () => {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().split("T")[0]; // 2026-01-02
|
||||
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-"); // 23-24-21
|
||||
const modeLabel = mode === "crisis" ? "crise" : "normal";
|
||||
const filename = `${date}_${time}_dump_${modeLabel}.md`;
|
||||
|
||||
const modeEmoji = mode === "crisis" ? "🆘" : "🌿";
|
||||
|
||||
let markdown = `# ${modeEmoji} Brain Dump - ${
|
||||
mode === "crisis" ? "CRISE" : "NORMAL"
|
||||
}\n\n`;
|
||||
markdown += `📅 **Date:** ${now.toLocaleString("fr-FR")}\n\n`;
|
||||
|
||||
sessionAnswers.forEach((answer) => {
|
||||
if (answer.answer.trim()) {
|
||||
markdown += `## ${answer.category}\n**Q:** ${answer.question}\n**R:** ${answer.answer}\n\n`;
|
||||
}
|
||||
});
|
||||
|
||||
const blob = new Blob([markdown], { type: "text/markdown" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const getQuestions = (questionMode: QuestionMode) => {
|
||||
const modeQuestions = dbQuestions.filter(
|
||||
(q) => q.mode === questionMode
|
||||
);
|
||||
if (modeQuestions.length > 0) {
|
||||
return modeQuestions;
|
||||
}
|
||||
return questionMode === "crisis" ? CRISIS_QUESTIONS : NORMAL_QUESTIONS;
|
||||
};
|
||||
|
||||
const questions = getQuestions(mode);
|
||||
|
||||
const handleDashboardBack = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/questions");
|
||||
const data = await response.json();
|
||||
setDbQuestions(data);
|
||||
} catch (error) {
|
||||
console.error("Error reloading questions:", error);
|
||||
}
|
||||
setScreen("home");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === "loading" && (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "authenticated" && (
|
||||
<>
|
||||
<div className="fixed top-4 right-4 z-50 flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 bg-gray-800/80 backdrop-blur-sm px-3 py-2 rounded-lg border border-gray-700">
|
||||
{session?.user?.image && (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt="Avatar"
|
||||
className="w-6 h-6 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm text-gray-300">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/login" })}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-400 px-3 py-2 rounded-lg text-sm transition-colors border border-red-500/30"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Confetti show={showConfetti} />
|
||||
|
||||
{screen === "home" && (
|
||||
<HomePage
|
||||
onStartNormal={() => startQuestionnaire(false)}
|
||||
onStartCrisis={() => startQuestionnaire(true)}
|
||||
onOpenKanban={() => setScreen("kanban")}
|
||||
onOpenDashboard={() => setScreen("dashboard")}
|
||||
isAdmin={session?.user?.isAdmin}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === "questions" && (
|
||||
<Questionnaire
|
||||
questions={questions}
|
||||
mode={mode}
|
||||
onComplete={handleQuestionnaireComplete}
|
||||
onBack={() => setScreen("home")}
|
||||
onAddTask={handleAddTask}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === "complete" && (
|
||||
<CompletePage
|
||||
answers={sessionAnswers}
|
||||
mode={mode}
|
||||
onGoHome={() => setScreen("home")}
|
||||
onGoToKanban={() => setScreen("kanban")}
|
||||
onExport={exportSession}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === "kanban" && (
|
||||
<KanbanBoard onBack={() => setScreen("home")} />
|
||||
)}
|
||||
|
||||
{screen === "dashboard" && (
|
||||
<QuestionsDashboard onBack={handleDashboardBack} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function UnauthorizedPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-gray-800 rounded-2xl p-8 max-w-md w-full shadow-2xl border border-red-500/30"
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: "spring", duration: 0.8 }}
|
||||
className="text-6xl mb-4"
|
||||
>
|
||||
🚫
|
||||
</motion.div>
|
||||
<h1 className="text-3xl font-bold text-red-400 mb-2">
|
||||
Accès Refusé
|
||||
</h1>
|
||||
<p className="text-gray-400 mb-4">
|
||||
Vous n'êtes pas autorisé à accéder à cette
|
||||
application.
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Cette application est réservée aux utilisateurs
|
||||
autorisés uniquement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href="/login">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="w-full bg-gray-700 hover:bg-gray-600 text-white font-semibold py-3 px-6 rounded-xl transition-colors duration-200"
|
||||
>
|
||||
Réessayer avec un autre compte
|
||||
</motion.button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user