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 (
+
+
+ );
+}
+
+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");