From 6951e662c95d6b5579f8b3e88a08ffed8fe663ad Mon Sep 17 00:00:00 2001 From: Dominik Natter Date: Wed, 26 Mar 2025 20:56:56 +0100 Subject: [PATCH] add: Layout, working permissions for papers --- .../_components/DiplomarbeitSearch.tsx | 29 ++-- src/app/(frontend)/_components/Footer.tsx | 18 ++- src/app/(frontend)/diplomarbeit/[id]/page.tsx | 35 +++++ src/app/(frontend)/layout.tsx | 10 +- src/app/(frontend)/natter-logo-black.svg | 24 +++ src/app/(frontend)/natter-logo-white.svg | 24 +++ src/app/(frontend)/page.tsx | 41 ++--- src/app/(payload)/api/diplomarbeiten/route.ts | 5 +- src/collections/Media.ts | 10 +- src/collections/Papers.ts | 143 ++++++++++-------- src/collections/Technologies.ts | 33 +++- src/payload-types.ts | 82 +++++++--- 12 files changed, 308 insertions(+), 146 deletions(-) create mode 100644 src/app/(frontend)/diplomarbeit/[id]/page.tsx create mode 100644 src/app/(frontend)/natter-logo-black.svg create mode 100644 src/app/(frontend)/natter-logo-white.svg diff --git a/src/app/(frontend)/_components/DiplomarbeitSearch.tsx b/src/app/(frontend)/_components/DiplomarbeitSearch.tsx index 487ad32..b5bdf6b 100644 --- a/src/app/(frontend)/_components/DiplomarbeitSearch.tsx +++ b/src/app/(frontend)/_components/DiplomarbeitSearch.tsx @@ -1,29 +1,40 @@ "use client"; import { useState, useEffect } from 'react'; import { Input } from '@/components/ui/input'; +import Link from 'next/link'; + export default function DiplomarbeitSearch() { const [search, setSearch] = useState(''); - const [results, setResults] = useState([]); + const [results, setResults] = useState<[]>([]); useEffect(() => { const timeoutId = setTimeout(async () => { const response = await fetch(`/api/diplomarbeiten?search=${search}`); const data = await response.json(); - setResults(data.titles || []); + setResults(data.docs || []); }, 500); // 500ms cooldown period return () => clearTimeout(timeoutId); }, [search]); return ( -
- setSearch(e.target.value)} className="w-full" /> - {results.map((title, index) => ( -
-

{title}

-
- ))} +
+
+

Diplomarbeitensuche

+ setSearch(e.target.value)} className="w-full" /> +
+ +
+ {results.map((result, index) => ( + +
+

{result.title}

+

{result.year}

+
+ + ))} +
); } diff --git a/src/app/(frontend)/_components/Footer.tsx b/src/app/(frontend)/_components/Footer.tsx index 9da372a..c3734dc 100644 --- a/src/app/(frontend)/_components/Footer.tsx +++ b/src/app/(frontend)/_components/Footer.tsx @@ -1,15 +1,27 @@ import React from 'react' +import Image from 'next/image' +import natterLogoBlack from "@/app/(frontend)/natter-logo.svg" +import natterLogoWhite from "@/app/(frontend)/natter-logo-white.svg" + export default function Footer() { + const currentYear = new Date().getFullYear(); return (
-

HTL Dornbirn

+

Made with {"<3"} by

+ + {"Natter +
-

© 2021 HTL Dornbirn

+

© {currentYear} HTL Dornbirn

- ) + ); } diff --git a/src/app/(frontend)/diplomarbeit/[id]/page.tsx b/src/app/(frontend)/diplomarbeit/[id]/page.tsx new file mode 100644 index 0000000..fdd514e --- /dev/null +++ b/src/app/(frontend)/diplomarbeit/[id]/page.tsx @@ -0,0 +1,35 @@ +import { getPayload } from 'payload'; +import config from '@payload-config'; +import Link from "next/link" + +export default async function Page({ + params, + }: { + params: Promise<{ id: number }> +}) { + const { id } = await params + + + const payload = await getPayload({ config }); + + + const result = await payload.findByID({ + collection: 'papers', // required + id: id, // required + }) + + + return ( +
+ {"<-"} Back +

Diplom- und Abschlussarbeiten ({result.year})

+

{result.title}

+

Zielsetzung

+

{result.goal}

+

Problemstellung

+

{result.issue}

+

Ergebnisse

+

{result.result}

+
+ ) +} diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 66bb9b5..4896a13 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -4,8 +4,8 @@ import Header from "@/app/(frontend)/_components/Header"; import Footer from "@/app/(frontend)/_components/Footer"; export const metadata = { - description: "A blank template using Payload in a Next.js app.", - title: "Payload Blank Template", + description: "Diplomarbeiten - HTL Dornbirn", + title: "Diplomarbeiten - HTL Dornbirn", }; export default async function RootLayout(props: { children: React.ReactNode }) { @@ -13,10 +13,12 @@ export default async function RootLayout(props: { children: React.ReactNode }) { return ( - + +
-
{children}
+
{children}
+
); diff --git a/src/app/(frontend)/natter-logo-black.svg b/src/app/(frontend)/natter-logo-black.svg new file mode 100644 index 0000000..6323d22 --- /dev/null +++ b/src/app/(frontend)/natter-logo-black.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/(frontend)/natter-logo-white.svg b/src/app/(frontend)/natter-logo-white.svg new file mode 100644 index 0000000..5986e94 --- /dev/null +++ b/src/app/(frontend)/natter-logo-white.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index f969dbb..130123f 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -1,45 +1,22 @@ -import { auth } from "@/auth"; -import { getPayloadSession } from "payload-authjs"; -import { getPayload } from 'payload' -import config from '@payload-config' -import { Input } from '@/components/ui/input' + import DiplomarbeitSearch from '@/app/(frontend)/_components/DiplomarbeitSearch' -const payload = await getPayload({ config }) + const Page = async () => { - const authjsSession = await auth(); - const payloadSession = await getPayloadSession(); - - - const media = await payload.find({ - collection: 'media', - }) - return (
-
-
-

Alle Diplomarbeiten

- -
-
- {media.docs.map((med) => ( -
-

{med.url}

-

{med.alt}

-
- ))} -
-

Auth.js Session

-
{JSON.stringify(authjsSession, null, 2)}
-
-

Payload CMS Session

-
{JSON.stringify(payloadSession, null, 2)}
+ + + + + + +
); }; diff --git a/src/app/(payload)/api/diplomarbeiten/route.ts b/src/app/(payload)/api/diplomarbeiten/route.ts index 878ffec..9ff9779 100644 --- a/src/app/(payload)/api/diplomarbeiten/route.ts +++ b/src/app/(payload)/api/diplomarbeiten/route.ts @@ -26,7 +26,6 @@ export async function GET(req: NextRequest) { }, }); - - const titles = response.docs.map((doc) => doc.title); - return NextResponse.json({ titles }); + const docs = response.docs.map((doc) => ({ title: doc.title, year: doc.year, id: doc.id })); + return NextResponse.json({ docs }); } diff --git a/src/collections/Media.ts b/src/collections/Media.ts index 019de1f..89972a2 100644 --- a/src/collections/Media.ts +++ b/src/collections/Media.ts @@ -23,15 +23,7 @@ export const Media: CollectionConfig = { type: 'text', //required: true, }, - { - name: 'caption', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()] - }, - }), - }, + ], upload: { // Upload to the public/media directory in Next.js making them publicly accessible even outside of Payload diff --git a/src/collections/Papers.ts b/src/collections/Papers.ts index df92492..a6e635a 100644 --- a/src/collections/Papers.ts +++ b/src/collections/Papers.ts @@ -1,133 +1,154 @@ -import type { CollectionConfig } from 'payload' +import type { CollectionConfig } from "payload"; + + export const Papers: CollectionConfig = { - slug: 'papers', + slug: "papers", labels: { - singular: 'Diplomarbeit', - plural: 'Diplomarbeiten', + singular: "Diplomarbeit", + plural: "Diplomarbeiten", }, access: { - create: ({ req: { user } }) => { - return Boolean(user?.type == "admin") // <-- Check if the user is authenticated - }, - delete: ({ req: { user } }) => { - return Boolean(user?.type == "admin") // <-- Check if the user is authenticated - }, - update: async ({ req: { user }, id, findByID }) => { - if (user?.type == "admin") return true; // Admins can update any paper + create: ({ req: { user } }) => Boolean(user?.type === "admin"), + delete: ({ req: { user } }) => Boolean(user?.type === "admin"), + update: async ({ req: { user, payload }, id }) => { + if (user?.type === "admin") return true; - const paper = await findByID({ collection: 'papers', id }); - return paper.authors.some(author => author.user === user.id); // Check if the user is an author + if (!user || !id) return false; // Explicitly handle missing ID + + const paper = await payload.findByID({ + collection: 'papers', + id, + depth: 1, + }); + + if (!paper) return false; + + return paper.authors.some((author: any) => author.user.id === user.id); }, + + }, admin: { - useAsTitle: 'title', + useAsTitle: "title", }, fields: [ { - name: 'title', - type: 'text', + name: "title", + type: "text", required: true, }, { - name: 'issue', - label: 'Problemstellung', - type: 'textarea', + name: "year", + type: "text", required: true, }, { - name: 'goal', - label: 'Zielsetzung', - type: 'textarea', + name: "issue", + label: "Problemstellung", + type: "textarea", required: true, }, { - name: 'technologies', - type: 'array', + name: "goal", + label: "Zielsetzung", + type: "textarea", + required: true, + }, + { + name: "result", + label: "Ergebnis", + type: "textarea", + required: true, + }, + { + name: "technologies", + type: "array", fields: [ { - name: 'technology', - type: 'relationship', - relationTo: 'technologies', + name: "technology", + type: "relationship", + relationTo: "technologies", required: true, }, { - name: 'description', - type: 'text', + name: "description", + type: "text", required: true, admin: { - placeholder: '... wurde für das Frontend verwendet', - } + placeholder: "... wurde für das Frontend verwendet", + }, }, ], }, { - name: 'prototype', - type: 'group', + name: "prototype", + type: "group", fields: [ { - name: 'image', - type: 'upload', - relationTo: 'media', + name: "image", + type: "upload", + relationTo: "media", required: true, }, { - name: 'description', - type: 'text', + name: "description", + type: "text", required: true, }, ], }, { - name: 'authors', - type: 'array', - label: 'Projektmitglieder', + name: "authors", + type: "array", + label: "Projektmitglieder", required: true, fields: [ { - name: 'profilePicture', - type: 'upload', - relationTo: 'media', + name: "profilePicture", + type: "upload", + relationTo: "media", required: true, }, { - name: 'user', - type: 'relationship', - relationTo: 'users', + name: "user", + type: "relationship", + relationTo: "users", required: true, }, { - name: 'position', - type: 'select', + name: "position", + type: "select", required: true, options: [ { - label: 'Projektleiter (PL)', - value: 'leader', + label: "Projektleiter (PL)", + value: "leader", }, { - label: 'Projektmitarbeiter (PM)', - value: 'member', + label: "Projektmitarbeiter (PM)", + value: "member", }, ], }, { - name: 'description', - type: 'text', + name: "description", + type: "text", required: true, }, - ], validate: (authors) => { // @ts-ignore - const leaders = authors.filter(author => author.position === 'leader') + const leaders = authors.filter( + (author) => author.position === "leader", + ); if (leaders.length > 1) { - return 'Only one author can be the project leader.' + return "Only one author can be the project leader."; } - return true + return true; }, }, ], -} +}; diff --git a/src/collections/Technologies.ts b/src/collections/Technologies.ts index f987dc0..44e2610 100644 --- a/src/collections/Technologies.ts +++ b/src/collections/Technologies.ts @@ -1,6 +1,11 @@ import type { CollectionConfig } from 'payload' +import path from 'path' +import { fileURLToPath } from 'url' +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + export const Technologies: CollectionConfig = { slug: 'technologies', labels: { @@ -11,7 +16,10 @@ export const Technologies: CollectionConfig = { useAsTitle: 'name', }, access: { - +read: () => true, + create: ({ req: { user } }) => Boolean(user), + update: ({ req: { user } }) => Boolean(user?.type === "admin"), + delete: ({ req: { user } }) => Boolean(user?.type === "admin"), }, fields: [ { @@ -24,11 +32,22 @@ export const Technologies: CollectionConfig = { type: 'textarea', required: true, }, - { - name: 'icon', - type: 'upload', - relationTo: 'media', - required: true, - }, ], + upload: { + // Upload to the public/media directory in Next.js making them publicly accessible even outside of Payload + staticDir: path.resolve(dirname, '../../public/technology-icons'), + adminThumbnail: 'thumbnail', + focalPoint: true, + imageSizes: [ + { + name: 'thumbnail', + width: 300, + }, + { + name: 'square', + width: 500, + height: 500, + }, + ], + }, } diff --git a/src/payload-types.ts b/src/payload-types.ts index f131c2e..3ebb1b3 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -124,8 +124,10 @@ export interface UserAuthOperations { export interface Paper { id: number; title: string; + year: string; issue: string; goal: string; + result: string; technologies?: | { technology: number | Technology; @@ -155,9 +157,35 @@ export interface Technology { id: number; name: string; description: string; - icon: number | Media; updatedAt: string; createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; + sizes?: { + thumbnail?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + square?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + }; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -166,21 +194,6 @@ export interface Technology { export interface Media { id: number; alt?: string | null; - caption?: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - } | null; updatedAt: string; createdAt: string; url?: string | null; @@ -344,8 +357,10 @@ export interface PayloadMigration { */ export interface PapersSelect { title?: T; + year?: T; issue?: T; goal?: T; + result?: T; technologies?: | T | { @@ -378,9 +393,41 @@ export interface PapersSelect { export interface TechnologiesSelect { name?: T; description?: T; - icon?: T; updatedAt?: T; createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; + sizes?: + | T + | { + thumbnail?: + | T + | { + url?: T; + width?: T; + height?: T; + mimeType?: T; + filesize?: T; + filename?: T; + }; + square?: + | T + | { + url?: T; + width?: T; + height?: T; + mimeType?: T; + filesize?: T; + filename?: T; + }; + }; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -410,7 +457,6 @@ export interface UsersSelect { */ export interface MediaSelect { alt?: T; - caption?: T; updatedAt?: T; createdAt?: T; url?: T;