v1.0 working #1

Merged
dominiknatter merged 6 commits from dev into main 2025-03-27 17:06:30 +00:00
12 changed files with 308 additions and 146 deletions
Showing only changes of commit 6951e662c9 - Show all commits

View File

@@ -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<string[]>([]);
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 (
<div className="w-full">
<Input type="text" value={search} placeholder="Suche" onChange={(e) => setSearch(e.target.value)} className="w-full" />
{results.map((title, index) => (
<div key={index}>
<h2>{title}</h2>
</div>
))}
<div className="w-full flex flex-col gap-16">
<div className="w-1/3 flex flex-col items-center mx-auto">
<h2 className="text-5xl w-auto pb-2">Diplomarbeitensuche</h2>
<Input type="text" value={search} placeholder="Suche" onChange={(e) => setSearch(e.target.value)} className="w-full" />
</div>
<div className="flex flex-col justify-center mx-auto w-2/3">
{results.map((result, index) => (
<Link key={index} href={"/diplomarbeit/" + result.id}>
<div key={index}>
<h2 className="text-3xl">{result.title}</h2>
<h3 className="text-xl">{result.year}</h3>
</div>
</Link>
))}
</div>
</div>
);
}

View File

@@ -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 (
<footer className="w-full h-20 bg-htl-red flex flex-row items-center p-2 gap-4 shadow-lg justify-between">
<div className="w-auto flex items-center flex-row gap-4">
<p className="text-6xl text-white">HTL Dornbirn</p>
<p className="text-xl text-white">Made with {"<3"} by</p>
<a href="https://natter.li" target="_blank">
<Image
className="h-6 w-auto"
src={natterLogoWhite}
alt={"Natter Logo"}
/>
</a>
</div>
<div className="w-auto">
<p className="text-white">© 2021 HTL Dornbirn</p>
<p className="text-white">© {currentYear} HTL Dornbirn</p>
</div>
</footer>
)
);
}

View File

@@ -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 (
<div className="w-2/3 mx-auto">
<Link href="/">{"<-"} Back</Link>
<h4 className="text-lg text-gray-600">Diplom- und Abschlussarbeiten ({result.year})</h4>
<h1 className="text-4xl font-semibold">{result.title}</h1>
<h2 className="text-3xl">Zielsetzung</h2>
<p className="text-lg">{result.goal}</p>
<h2 className="text-3xl">Problemstellung</h2>
<p className="text-lg">{result.issue}</p>
<h2 className="text-3xl">Ergebnisse</h2>
<p className="text-lg">{result.result}</p>
</div>
)
}

View File

@@ -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 (
<html lang="de-AT">
<body className="w-full min-h-full">
<body>
<div className="flex flex-col min-h-screen">
<Header />
<main>{children}</main>
<main className="grow">{children}</main>
<Footer />
</div>
</body>
</html>
);

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 479.47 75">
<defs>
<style>
.cls-1 {
fill: #231f20;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer_1">
<g>
<polygon class="cls-1" points="0 75 37.5 75 37.5 37.5 0 0 0 75"/>
<polygon class="cls-1" points="37.5 0 37.5 37.5 75 75 75 0 37.5 0"/>
</g>
<g>
<path class="cls-1" d="M87.46,73.48V1.38h9.99l44.08,59.02-1.65.93c-.48-2.81-.88-6.08-1.18-9.79s-.53-7.79-.67-12.26c-.14-4.46-.21-9.2-.21-14.21V1.38h11.12v72.1h-10.2l-43.36-56.86,1.13-1.44c.48,5.22.86,9.49,1.13,12.82.27,3.33.48,5.99.62,7.98.14,1.99.22,3.55.26,4.69.03,1.13.05,2.08.05,2.83v29.97h-11.12Z"/>
<path class="cls-1" d="M161.62,73.48L189.53,1.38h11.33l27.71,72.1h-11.85l-16.48-43.47c-.21-.48-.6-1.65-1.18-3.5-.58-1.85-1.25-3.91-2.01-6.18-.76-2.27-1.44-4.34-2.06-6.23-.62-1.89-1.03-3.11-1.24-3.66l2.37-.1c-.41,1.17-.91,2.64-1.49,4.43-.58,1.79-1.2,3.67-1.85,5.67-.65,1.99-1.29,3.86-1.91,5.61-.62,1.75-1.13,3.21-1.54,4.38l-16.38,43.05h-11.33ZM173.98,55.56l4.12-10.71h32.75l4.74,10.71h-41.61Z"/>
<path class="cls-1" d="M254.93,73.48V12.09h-20.19V1.38h52.12v10.71h-20.81v61.39h-11.12Z"/>
<path class="cls-1" d="M314.26,73.48V12.09h-20.19V1.38h52.12v10.71h-20.81v61.39h-11.12Z"/>
<path class="cls-1" d="M359.89,73.48V1.38h45.42v10.71h-34.3v50.68h34.3v10.71h-45.42ZM364.94,41.86v-10.71h34.71v10.71h-34.71Z"/>
<path class="cls-1" d="M423.13,73.48V1.38h30.69c4.12,0,7.91,1.01,11.38,3.04,3.47,2.03,6.23,4.77,8.29,8.24,2.06,3.47,3.09,7.36,3.09,11.69,0,3.91-1.03,7.55-3.09,10.92-2.06,3.37-4.81,6.06-8.24,8.08-3.43,2.03-7.25,3.04-11.43,3.04h-19.57v27.09h-11.12ZM434.26,35.68h20.6c1.99,0,3.78-.5,5.36-1.49,1.58-.99,2.82-2.37,3.71-4.12.89-1.75,1.34-3.69,1.34-5.82,0-2.4-.57-4.51-1.7-6.33-1.13-1.82-2.71-3.25-4.74-4.27-2.03-1.03-4.31-1.54-6.85-1.54h-17.72v23.59ZM465.98,73.48l-18.33-32.55,11.43-2.58,20.39,35.23-13.49-.1Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 479.47 75">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer_1">
<g>
<polygon class="cls-1" points="0 75 37.5 75 37.5 37.5 0 0 0 75"/>
<polygon class="cls-1" points="37.5 0 37.5 37.5 75 75 75 0 37.5 0"/>
</g>
<g>
<path class="cls-1" d="M87.46,74.5V2.4h9.99l44.08,59.02-1.65.93c-.48-2.81-.88-6.08-1.18-9.79s-.53-7.79-.67-12.26c-.14-4.46-.21-9.2-.21-14.21V2.4h11.12v72.1h-10.2l-43.36-56.86,1.13-1.44c.48,5.22.86,9.49,1.13,12.82.27,3.33.48,5.99.62,7.98.14,1.99.22,3.55.26,4.69.03,1.13.05,2.08.05,2.83v29.97h-11.12Z"/>
<path class="cls-1" d="M161.62,74.5L189.53,2.4h11.33l27.71,72.1h-11.85l-16.48-43.47c-.21-.48-.6-1.65-1.18-3.5-.58-1.85-1.25-3.91-2.01-6.18-.76-2.27-1.44-4.34-2.06-6.23-.62-1.89-1.03-3.11-1.24-3.66l2.37-.1c-.41,1.17-.91,2.64-1.49,4.43-.58,1.79-1.2,3.67-1.85,5.67-.65,1.99-1.29,3.86-1.91,5.61-.62,1.75-1.13,3.21-1.54,4.38l-16.38,43.05h-11.33ZM173.98,56.58l4.12-10.71h32.75l4.74,10.71h-41.61Z"/>
<path class="cls-1" d="M254.93,74.5V13.11h-20.19V2.4h52.12v10.71h-20.81v61.39h-11.12Z"/>
<path class="cls-1" d="M314.26,74.5V13.11h-20.19V2.4h52.12v10.71h-20.81v61.39h-11.12Z"/>
<path class="cls-1" d="M359.89,74.5V2.4h45.42v10.71h-34.3v50.68h34.3v10.71h-45.42ZM364.94,42.88v-10.71h34.71v10.71h-34.71Z"/>
<path class="cls-1" d="M423.13,74.5V2.4h30.69c4.12,0,7.91,1.01,11.38,3.04,3.47,2.03,6.23,4.77,8.29,8.24,2.06,3.47,3.09,7.36,3.09,11.69,0,3.91-1.03,7.55-3.09,10.92-2.06,3.37-4.81,6.06-8.24,8.08-3.43,2.03-7.25,3.04-11.43,3.04h-19.57v27.09h-11.12ZM434.26,36.7h20.6c1.99,0,3.78-.5,5.36-1.49,1.58-.99,2.82-2.37,3.71-4.12.89-1.75,1.34-3.69,1.34-5.82,0-2.4-.57-4.51-1.7-6.33-1.13-1.82-2.71-3.25-4.74-4.27-2.03-1.03-4.31-1.54-6.85-1.54h-17.72v23.59ZM465.98,74.5l-18.33-32.55,11.43-2.58,20.39,35.23-13.49-.1Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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 (
<main>
<div className="w-full flex justify-center pt-4">
<div className="w-1/3 flex flex-col items-center">
<h2 className="text-5xl w-auto ">Alle Diplomarbeiten</h2>
<DiplomarbeitSearch />
</div>
</div>
{media.docs.map((med) => (
<div key={med.id}>
<h2>{med.url}</h2>
<p>{med.alt}</p>
</div>
))}
<br />
<h3>Auth.js Session</h3>
<pre>{JSON.stringify(authjsSession, null, 2)}</pre>
<br />
<h3>Payload CMS Session</h3>
<pre>{JSON.stringify(payloadSession, null, 2)}</pre>
<DiplomarbeitSearch />
</main>
);
};

View File

@@ -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 });
}

View File

@@ -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

View File

@@ -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;
},
},
],
}
};

View File

@@ -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,
},
],
},
}

View File

@@ -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<T extends boolean = true> {
title?: T;
year?: T;
issue?: T;
goal?: T;
result?: T;
technologies?:
| T
| {
@@ -378,9 +393,41 @@ export interface PapersSelect<T extends boolean = true> {
export interface TechnologiesSelect<T extends boolean = true> {
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<T extends boolean = true> {
*/
export interface MediaSelect<T extends boolean = true> {
alt?: T;
caption?: T;
updatedAt?: T;
createdAt?: T;
url?: T;