From cff9dad54ca6f49105718b126889c32d971f2278 Mon Sep 17 00:00:00 2001 From: mr-shortman Date: Sat, 15 Mar 2025 17:16:35 +0100 Subject: [PATCH] about page; added user profileform and me page --- src/app/(PAGES)/logipedia/page.tsx | 8 +++ src/app/(PAGES)/me/_components/me-page.tsx | 50 +++++++++++++ src/app/(PAGES)/me/_components/user-form.tsx | 71 +++++++++++++++++++ src/app/(PAGES)/me/page.tsx | 13 ++++ src/app/(PAGES)/page.tsx | 42 +++++++++-- src/components/article/article-filter-bar.tsx | 2 +- .../category/category-filter-bar.tsx | 2 +- src/components/layout/app-sidebar/index.tsx | 8 ++- .../layout/app-sidebar/nav-main.tsx | 9 +-- .../layout/app-sidebar/nav-user.tsx | 32 +++------ src/components/section-header.tsx | 21 ++++++ src/components/ui/alert.tsx | 59 +++++++++++++++ src/config/app.routes.ts | 5 ++ src/lib/validation/zod/user.ts | 7 ++ src/server/actions/user.ts | 11 +++ src/server/api/routers/article.ts | 4 +- src/server/api/routers/users.ts | 12 +++- 17 files changed, 321 insertions(+), 35 deletions(-) create mode 100644 src/app/(PAGES)/logipedia/page.tsx create mode 100644 src/app/(PAGES)/me/_components/me-page.tsx create mode 100644 src/app/(PAGES)/me/_components/user-form.tsx create mode 100644 src/app/(PAGES)/me/page.tsx create mode 100644 src/components/section-header.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/lib/validation/zod/user.ts diff --git a/src/app/(PAGES)/logipedia/page.tsx b/src/app/(PAGES)/logipedia/page.tsx new file mode 100644 index 0000000..33f650c --- /dev/null +++ b/src/app/(PAGES)/logipedia/page.tsx @@ -0,0 +1,8 @@ +import { appConfig } from "@/config"; +import React from "react"; + +function Page() { + return
Das ist {appConfig.name}
; +} + +export default Page; diff --git a/src/app/(PAGES)/me/_components/me-page.tsx b/src/app/(PAGES)/me/_components/me-page.tsx new file mode 100644 index 0000000..551ebf2 --- /dev/null +++ b/src/app/(PAGES)/me/_components/me-page.tsx @@ -0,0 +1,50 @@ +"use client"; +import { User } from "next-auth"; +import React from "react"; +import UserForm from "./user-form"; +import SectionHeader from "@/components/section-header"; +import Avatar from "@/components/avatar"; +import { Button } from "@/components/ui/button"; +import { Edit, XIcon } from "lucide-react"; + +function MePage({ user }: { user: User }) { + const [editProfile, setEditProfile] = React.useState(false); + return ( + <> + + + +
+ + +
+

{user.name}

+

{user.email}

+
+
+ {editProfile && ( +
+ setEditProfile(false)} server_user={user} /> +
+ )} + + ); +} + +export default MePage; diff --git a/src/app/(PAGES)/me/_components/user-form.tsx b/src/app/(PAGES)/me/_components/user-form.tsx new file mode 100644 index 0000000..9d7327c --- /dev/null +++ b/src/app/(PAGES)/me/_components/user-form.tsx @@ -0,0 +1,71 @@ +"use client"; +import React from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { z } from "zod"; +import { userProfileSchema } from "@/lib/validation/zod/user"; +import { User } from "next-auth"; +import { updateUserProfile } from "@/server/actions/user"; +import { toast } from "sonner"; + +function UserForm({ server_user, cb }: { server_user: User; cb?: () => void }) { + const form = useForm>({ + resolver: zodResolver(userProfileSchema), + defaultValues: { + name: server_user?.name ?? "", + }, + }); + + // 2. Define a submit handler. + async function onSubmit(values: z.infer) { + // Do something with the form values. + // ✅ This will be type-safe and validated. + const { success } = await updateUserProfile(values); + if (success) toast.success("Dein Profil wurde aktualisiert!"); + else toast.error("Etwas ist fehlgeschlagen. Bitte versuche es erneut."); + cb?.(); + form.reset(); + } + + return ( +
+ + ( + + Name + +
+ +
+
+ + Dein Name ist für alle sichtbar. + + +
+ )} + /> + + + + + ); +} + +export default UserForm; diff --git a/src/app/(PAGES)/me/page.tsx b/src/app/(PAGES)/me/page.tsx new file mode 100644 index 0000000..15cc3bc --- /dev/null +++ b/src/app/(PAGES)/me/page.tsx @@ -0,0 +1,13 @@ +import { appRoutes } from "@/config"; +import { auth } from "@/server/auth"; +import { redirect } from "next/navigation"; +import React from "react"; +import MePage from "./_components/me-page"; + +async function Page() { + const session = await auth(); + if (!session) return redirect(appRoutes.signin); + return ; +} + +export default Page; diff --git a/src/app/(PAGES)/page.tsx b/src/app/(PAGES)/page.tsx index 7764463..a1c4476 100644 --- a/src/app/(PAGES)/page.tsx +++ b/src/app/(PAGES)/page.tsx @@ -1,13 +1,47 @@ import { api } from "@/trpc/server"; -import MainPage from "./_components/main-page"; -import { Article } from "@/server/db/schema"; +import { appConfig, appRoutes } from "@/config"; +import CategoryGrid from "@/components/category/grid/category-grid"; +import ArticleGrid from "@/components/article/grid/article-grid"; +import SectionHeader from "@/components/section-header"; +import ArrowLink from "@/components/arrow-link"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Icons } from "@/components/icons"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; export default async function Home() { - const articles = await api.article.getAll({ limit: 12 }); const categories = await api.category.getAll({ limit: 6 }); + const articles = await api.article.getAll({ limit: 6 }); return ( <> - + +
+ +
+ + Wilkommen bei Logipedia! + + + Lorem ipsum, dolor sit amet consectetur adipisicing elit. + +
+ +
+
+ + + Alle Kategorien + + + + + Alle Artikel + + ); } diff --git a/src/components/article/article-filter-bar.tsx b/src/components/article/article-filter-bar.tsx index bcc113f..cc3bea4 100644 --- a/src/components/article/article-filter-bar.tsx +++ b/src/components/article/article-filter-bar.tsx @@ -75,7 +75,7 @@ function ArticleFilterBar({ "text-xs text-muted-foreground", hasFilter ? "flex lg:opacity-100" - : "hidden lg:opacity-0 lg:disabled:opacity-0", + : "hidden lg:flex lg:opacity-0 lg:disabled:opacity-0", )} onClick={() => onFilterChange(defaultFilter)} > diff --git a/src/components/category/category-filter-bar.tsx b/src/components/category/category-filter-bar.tsx index c9c6d0f..c656321 100644 --- a/src/components/category/category-filter-bar.tsx +++ b/src/components/category/category-filter-bar.tsx @@ -73,7 +73,7 @@ export default function CategoryFilterBar({ "text-xs text-muted-foreground", hasFilter ? "flex lg:opacity-100" - : "hidden lg:opacity-0 lg:disabled:opacity-0", + : "hidden lg:flex lg:opacity-0 lg:disabled:opacity-0", )} onClick={() => onFilterChange(defaultFilter)} > diff --git a/src/components/layout/app-sidebar/index.tsx b/src/components/layout/app-sidebar/index.tsx index 05235f0..a318419 100644 --- a/src/components/layout/app-sidebar/index.tsx +++ b/src/components/layout/app-sidebar/index.tsx @@ -16,7 +16,8 @@ import { User } from "next-auth"; import NavTeamSection from "./nav-team-section"; import NavBranding from "./nav-branding"; import { Icons } from "@/components/icons"; -import { appConfig } from "@/config"; +import { appConfig, appRoutes } from "@/config"; +import { Info } from "lucide-react"; export function AppSidebar({ ...props @@ -47,5 +48,10 @@ const data = { icon: Icons.discord, external: true, }, + { + title: `Was ist ${appConfig.name}`, + url: appRoutes.about, + icon: Info, + }, ], }; diff --git a/src/components/layout/app-sidebar/nav-main.tsx b/src/components/layout/app-sidebar/nav-main.tsx index 499691c..3cb4374 100644 --- a/src/components/layout/app-sidebar/nav-main.tsx +++ b/src/components/layout/app-sidebar/nav-main.tsx @@ -20,6 +20,7 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { api } from "@/trpc/react"; import { Icons } from "@/components/icons"; +import { appRoutes } from "@/config"; export function NavMain() { const [{ articles, categories }] = api.app.getSidebarMain.useSuspenseQuery(); @@ -32,11 +33,11 @@ export function NavMain() { items: [ ...articles.map((article) => ({ title: article.title, - url: `/artikel/${article.slug}`, + url: appRoutes.article(article.slug), })), { title: "Alle Artikel", - url: "/artikel", + url: appRoutes.allArticles, }, ], }, @@ -47,11 +48,11 @@ export function NavMain() { items: [ ...categories.map((category) => ({ title: category.name, - url: `/kategorie/${category.slug}`, + url: appRoutes.category(category.slug), })), { title: "Alle Kategorien", - url: "#", + url: appRoutes.allCategories, }, ], }, diff --git a/src/components/layout/app-sidebar/nav-user.tsx b/src/components/layout/app-sidebar/nav-user.tsx index 6dce667..60e8a25 100644 --- a/src/components/layout/app-sidebar/nav-user.tsx +++ b/src/components/layout/app-sidebar/nav-user.tsx @@ -7,6 +7,7 @@ import { CreditCard, LogOut, Sparkles, + User, } from "lucide-react"; import { @@ -81,30 +82,19 @@ export function NavUser({ user }: { user?: any }) { - - - Upgrade to Pro + + + + Profil + - - - - Account - - - - Billing - - - - Notifications - - - - - - Log out + + + + Abmelden + diff --git a/src/components/section-header.tsx b/src/components/section-header.tsx new file mode 100644 index 0000000..2cea78d --- /dev/null +++ b/src/components/section-header.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils"; +import React from "react"; + +function SectionHeader({ + text, + children, + className, +}: { + text: string; + children?: React.ReactNode; + className?: string; +}) { + return ( +
+

{text}

+ {children} +
+ ); +} + +export default SectionHeader; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/config/app.routes.ts b/src/config/app.routes.ts index f37215b..478ed40 100644 --- a/src/config/app.routes.ts +++ b/src/config/app.routes.ts @@ -2,6 +2,8 @@ export type RouteWithParam = (param: string) => string; export type Route = string | RouteWithParam; export type AppRoutes = { + about: string; + // Home and admin home: string; admin: { @@ -21,9 +23,11 @@ export type AppRoutes = { // Auth routes signin: string; signout: string; + profile: string; }; export const appRoutes: AppRoutes = { + about: "/logipedia", home: "/", admin: { base: "/admin" }, @@ -40,4 +44,5 @@ export const appRoutes: AppRoutes = { // auth signin: "/api/auth/signin", signout: "/api/auth/signout", + profile: "/me", }; diff --git a/src/lib/validation/zod/user.ts b/src/lib/validation/zod/user.ts new file mode 100644 index 0000000..c0f982a --- /dev/null +++ b/src/lib/validation/zod/user.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const userProfileSchema = z.object({ + name: z.string().min(1), + // image: z.string().optional(), + // email: z.string().email(), +}); diff --git a/src/server/actions/user.ts b/src/server/actions/user.ts index f8f43cf..bdc9415 100644 --- a/src/server/actions/user.ts +++ b/src/server/actions/user.ts @@ -1,7 +1,18 @@ "use server"; +import { userProfileSchema } from "@/lib/validation/zod/user"; import { api } from "@/trpc/server"; import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +export async function updateUserProfile( + profile: z.infer, +) { + const [result] = await api.users.updateProfile({ profile }); + if (!result?.id) return { success: false }; + revalidatePath("/me"); + return { success: true }; +} export async function setUserPermissions(userId: string, permission: number) { const result = await api.users.setPermission({ userId, permission }); diff --git a/src/server/api/routers/article.ts b/src/server/api/routers/article.ts index 5c6e36e..03ac1d6 100644 --- a/src/server/api/routers/article.ts +++ b/src/server/api/routers/article.ts @@ -160,7 +160,7 @@ export const articleRouter = createTRPCRouter({ .optional(), ) .query(async ({ ctx, input }) => { - return await ctx.db.query.articles.findMany({ + return (await ctx.db.query.articles.findMany({ where: input?.categoryId ? eq(articles.categoryId, input.categoryId) : undefined, @@ -170,7 +170,7 @@ export const articleRouter = createTRPCRouter({ slug: true, createdAt: true, }, - }); + })) as Array
; }), getCount: publicProcedure diff --git a/src/server/api/routers/users.ts b/src/server/api/routers/users.ts index 71f8087..fd643fd 100644 --- a/src/server/api/routers/users.ts +++ b/src/server/api/routers/users.ts @@ -1,11 +1,21 @@ import { hasPermission, Role } from "@/lib/validation/permissions"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { z } from "zod"; -import { permission } from "process"; import { users } from "@/server/db/schema"; import { desc, eq } from "drizzle-orm"; +import { userProfileSchema } from "@/lib/validation/zod/user"; export const usersRouter = createTRPCRouter({ + updateProfile: protectedProcedure + .input(z.object({ profile: userProfileSchema })) + .mutation(async ({ ctx, input }) => { + return await ctx.db + .update(users) + .set(input.profile) + .where(eq(users.id, ctx.session.user.id)) + .returning(); + }), + getAll: protectedProcedure.query(async ({ ctx }) => { const isAdmin = hasPermission(ctx.session.user.role, Role.ADMIN); if (!isAdmin) throw new Error("You are not allowed to get all users");