diff --git a/README.md b/README.md index e215bc4..4828f25 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,83 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` +aystone +├─ app +│ ├─ a-propos +│ │ └─ page.tsx +│ ├─ ads +│ │ └─ page.tsx +│ ├─ api +│ │ ├─ auth +│ │ │ └─ [...nextauth] +│ │ │ └─ route.ts +│ │ └─ planning +│ │ └─ route.ts +│ ├─ dashboard +│ │ ├─ Sidebar.tsx +│ │ ├─ StatCard.tsx +│ │ ├─ actions.ts +│ │ ├─ ajouter +│ │ │ ├─ AddProjectForm.tsx +│ │ │ └─ page.tsx +│ │ ├─ layout.tsx +│ │ ├─ page.tsx +│ │ ├─ profil +│ │ │ └─ page.tsx +│ │ └─ projets +│ │ ├─ [id] +│ │ │ ├─ EditProjectForm.tsx +│ │ │ └─ page.tsx +│ │ └─ page.tsx +│ ├─ globals.css +│ ├─ layout.tsx +│ ├─ login +│ │ └─ page.tsx +│ ├─ maps +│ │ └─ page.tsx +│ ├─ page.tsx +│ └─ providers.tsx +├─ auth.ts +├─ components +│ ├─ AdsGalleryClient.tsx +│ ├─ AdsSideBanners.tsx +│ ├─ AvailabilityPlanner.tsx +│ ├─ Banner.tsx +│ ├─ Footer.tsx +│ ├─ MapsClient.tsx +│ ├─ McSplash.tsx +│ ├─ MotionHeading.tsx +│ ├─ Navbar.tsx +│ ├─ ProjectsTable.tsx +│ └─ RedstoneParticles.tsx +├─ eslint.config.mjs +├─ lib +│ ├─ getAds.ts +│ └─ prisma.ts +├─ middleware.ts +├─ next.config.ts +├─ package-lock.json +├─ package.json +├─ postcss.config.mjs +├─ prisma +│ ├─ migrations +│ │ ├─ 20250727013821_init +│ │ │ └─ migration.sql +│ │ ├─ 20250727025700_add_mcname +│ │ │ └─ migration.sql +│ │ ├─ 20250727031005_project +│ │ │ └─ migration.sql +│ │ ├─ 20250727203115_add_availability_model +│ │ │ └─ migration.sql +│ │ └─ migration_lock.toml +│ └─ schema.prisma +├─ public +│ ├─ ads +│ │ ├─ riseofkinkdom.jpeg +│ │ └─ voat.png +│ ├─ aystone-saison2.png +│ └─ aystone.png +├─ tsconfig.json +└─ types + └─ next-auth.d.ts -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +``` \ No newline at end of file diff --git a/app/a-propos/page.tsx b/app/a-propos/page.tsx new file mode 100644 index 0000000..d879b1f --- /dev/null +++ b/app/a-propos/page.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const TAGS = [ + { id: 'spawn', label: 'Spawn', cls: 'bg-purple-700/30 text-purple-300' }, + { id: 'build', label: 'Build', cls: 'bg-emerald-600/25 text-emerald-300' }, + { id: 'usine', label: 'Usine', cls: 'bg-orange-600/20 text-orange-300' }, +]; + +export default function AProposPage() { + return ( +
+
+ + À propos + + + +

+ Cette application web permet à la communauté du serveur  + Aystone de référencer, suivre + et partager ses projets Minecraft dans les différents mondes : + Overworld, + Nether, + End. +

+ +

+ Le tableau liste : joueur, état, monde, nom, description, + coordonnées et tags personnalisés. L’interface responsive et + épurée garantit une expérience fluide sur mobile comme sur desktop. +

+ +

+ Sous le capot : Next 15, React 19, + Tailwind 4 et Framer Motion pour les + animations. Aucune donnée sensible n’est stockée : on se connecte + simplement via discord . +
+ Sur le dashboard, vous pouvez ajouter votre pseudo Minecraft dans la section Infos du compte +

+ +
+

Légende des tags

+
+ {TAGS.map(t => ( + + {t.label} + + ))} +
+
+ +
+

Code source

+

+ Le projet est entièrement open-source :{" "} + + dépôt GitHub + . + Déployé sur Coolify. +

+
+
+
+
+ ); +} diff --git a/app/ads/page.tsx b/app/ads/page.tsx new file mode 100644 index 0000000..e3e45a5 --- /dev/null +++ b/app/ads/page.tsx @@ -0,0 +1,11 @@ +import AdsGalleryClient from "@/components/AdsGalleryClient"; +import { getAdFiles } from "@/lib/getAds"; + +export const metadata = { + title: "Galerie des publicités – Aystone", +}; + +export default async function AdsGalleryPage() { + const files = await getAdFiles(); + return ; +} \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..020852e --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; \ No newline at end of file diff --git a/app/couleurs/page.tsx b/app/couleurs/page.tsx new file mode 100644 index 0000000..21aed71 --- /dev/null +++ b/app/couleurs/page.tsx @@ -0,0 +1,23 @@ +import Image from "next/image"; + +export const metadata = { + title: "Charte des couleurs Minecraft", +}; +export default function CouleursPage() { + return ( +
+

+ 🎨 Charte des couleurs Minecraft +

+
+ Charte des couleurs Minecraft +
+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/Sidebar.tsx b/app/dashboard/Sidebar.tsx new file mode 100644 index 0000000..bd14a49 --- /dev/null +++ b/app/dashboard/Sidebar.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + LayoutDashboard, + PlusCircle, + TableProperties, + User +} from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const LINKS = [ + { name: "Tableau", href: "/dashboard", icon: LayoutDashboard }, + { name: "Profil", href: "/dashboard/profil", icon: User }, + { name: "Ajouter", href: "/dashboard/ajouter", icon: PlusCircle }, + { name: "Mes Projets", href: "/dashboard/projets", icon: TableProperties }, +]; + +export default function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} \ No newline at end of file diff --git a/app/dashboard/StatCard.tsx b/app/dashboard/StatCard.tsx new file mode 100644 index 0000000..00ed09e --- /dev/null +++ b/app/dashboard/StatCard.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { motion } from "framer-motion"; +import { ReactNode } from "react"; + +type Props = { + title: string; + value: string | number; + icon?: ReactNode; +}; + +export default function StatCard({ title, value, icon }: Props) { + return ( + + {icon && ( +
+ {icon} +
+ )} +

{title}

+

{value}

+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/actions.ts b/app/dashboard/actions.ts new file mode 100644 index 0000000..5ab2590 --- /dev/null +++ b/app/dashboard/actions.ts @@ -0,0 +1,90 @@ +"use server"; + +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +export async function createProject(form: FormData) { + const session = await auth(); + if (!session) throw new Error("unauth"); + + const currentUser = await prisma.user.findUnique({ + where: { email: session.user!.email! }, + }); + + const joueur = + (form.get("joueur") as string | null)?.trim() || + currentUser?.mcName || + session.user!.name!; + + const coords = form.get("coords")?.toString().trim(); + const tagsRaw = form.get("tags")?.toString() ?? ""; + + const data = { + joueur, + etat: (form.get("etat") as string) ?? "En cours", + monde: (form.get("monde") as string) ?? "Overworld", + projet: (form.get("projet") as string).trim(), + description: (form.get("description") as string).trim(), + coords: coords && coords.length > 0 ? coords : null, + tags: tagsRaw + .split(",") + .map((t) => t.trim()) + .filter(Boolean), + }; + + await prisma.project.create({ data }); + revalidatePath("/dashboard"); + redirect("/dashboard"); +} + +export async function updateProject(projectId: string, form: FormData) { + const session = await auth(); + if (!session) throw new Error("unauth"); + + const coords = form.get("coords")?.toString().trim(); + const tagsRaw = form.get("tags")?.toString() ?? ""; + + const data = { + etat: (form.get("etat") as string), + monde: (form.get("monde") as string), + projet: (form.get("projet") as string).trim(), + description: (form.get("description") as string).trim(), + coords: coords && coords.length > 0 ? coords : null, + tags: tagsRaw + .split(",") + .map((t) => t.trim()) + .filter(Boolean), + }; + + await prisma.project.update({ + where: { id: projectId }, + data, + }); + + revalidatePath("/dashboard"); + redirect("/dashboard"); +} + +export async function updateMinecraftName(form: FormData) { + const session = await auth(); + if (!session) throw new Error("unauth"); + + const mcName = (form.get("mcName") as string).trim(); + if (!/^[A-Za-z0-9_]{3,16}$/.test(mcName)) throw new Error("invalid"); + + await prisma.user.upsert({ + where: { email: session.user!.email! }, + update: { mcName }, + create: { + email: session.user!.email!, + name: session.user!.name, + image: session.user!.image, + mcName, + }, + }); + + revalidatePath("/dashboard"); + revalidatePath("/"); +} \ No newline at end of file diff --git a/app/dashboard/ajouter/AddProjectForm.tsx b/app/dashboard/ajouter/AddProjectForm.tsx new file mode 100644 index 0000000..d1e685c --- /dev/null +++ b/app/dashboard/ajouter/AddProjectForm.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Layers3, MapPin, Plus, Tags, User, X } from "lucide-react"; +import { useSession } from "next-auth/react"; +import { useState } from "react"; +import { createProject } from "../actions"; + +const PREDEF = ["build", "usine", "spawn"] as const; +type TagColor = (typeof PREDEF)[number]; + +const palette: Record = { + build: "bg-emerald-600/90", + usine: "bg-orange-600/90", + spawn: "bg-fuchsia-700/90", +}; + +const isValidCoord = (value: string) => /^-?\d*$/.test(value); + +export default function AddProjectForm() { + const { data: session } = useSession(); + const [tags, setTags] = useState([]); + const [tagInput, setInput] = useState(""); + + const [x, setX] = useState(""); + const [y, setY] = useState(""); + const [z, setZ] = useState(""); + + const addTag = (t: string) => { + const val = t.trim().toLowerCase(); + if (!val || tags.includes(val) || tags.length >= 5) return; + setTags([...tags, val]); + }; + + const del = (t: string) => setTags(tags.filter((x) => x !== t)); + + const getTagClass = (t: string) => { + return palette[t as TagColor] ?? "bg-gray-600"; + }; + + return ( + + {/* nom */} +
+ + +
+ + {/* joueur */} + {session?.user?.email === "ultralionfr@gmail.com" && ( +
+ + +
+ )} + + {/* coords */} +
+ {[ + { name: "x", value: x, setValue: setX }, + { name: "y", value: y, setValue: setY }, + { name: "z", value: z, setValue: setZ }, + ].map(({ name, value, setValue }) => ( +
+ + { + if (isValidCoord(e.target.value)) setValue(e.target.value); + }} + className="w-full rounded-lg bg-gray-700 py-2 pl-9 pr-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+ ))} +
+ + {/* Tags */} +
+ + +
+ {tags.map((t) => ( + + ))} +
+ +
+ + +
+ setInput(e.target.value)} + onKeyDown={(e) => + e.key === "Enter" && (e.preventDefault(), addTag(tagInput), setInput("")) + } + placeholder="Ajouter un tag personnalisé" + className="w-full rounded-lg bg-gray-700 py-2 pl-3 pr-12 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + +
+
+
+ + {/* description */} +