basic search working

This commit is contained in:
Dominik Natter
2025-03-26 17:41:46 +01:00
parent a3104cfeb0
commit c5c9cc37af
13 changed files with 406 additions and 18 deletions

View File

@@ -0,0 +1,29 @@
"use client";
import { useState, useEffect } from 'react';
import { Input } from '@/components/ui/input';
export default function DiplomarbeitSearch() {
const [search, setSearch] = useState('');
const [results, setResults] = useState<string[]>([]);
useEffect(() => {
const timeoutId = setTimeout(async () => {
const response = await fetch(`/api/diplomarbeiten?search=${search}`);
const data = await response.json();
setResults(data.titles || []);
}, 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>
);
}

View File

@@ -0,0 +1,15 @@
import React from 'react'
export default function Footer() {
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>
</div>
<div className="w-auto">
<p className="text-white">© 2021 HTL Dornbirn</p>
</div>
</footer>
)
}

View File

@@ -0,0 +1,27 @@
import Image from 'next/image'
import HTLDLogo from '@/app/(frontend)/htld.svg'
import { SignOutButton } from '@/app/(frontend)/_components/SignOutButton'
import { SignInButton } from '@/app/(frontend)/_components/SignInButton'
import React from 'react'
import { getPayloadSession } from 'payload-authjs'
export default async function Header(){
const session = await getPayloadSession();
return (
<header 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">
<Image
className="h-16 w-auto"
src={HTLDLogo}
alt={"HTL Dornbirn Logo"}
/>
<p className="text-6xl text-white">Diplom- und Abschlussarbeiten</p>
</div>
<div className="w-auto">
{session ? <SignOutButton /> : <SignInButton />}
</div>
</header>
)
}

View File

@@ -1,4 +1,5 @@
import { signIn } from "@/auth";
import { Button } from '@/components/ui/button'
export function SignInButton() {
return (
@@ -8,7 +9,7 @@ export function SignInButton() {
await signIn("github");
}}
>
<button type="submit">Sign In</button>
<Button type="submit">Sign In</Button>
</form>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import type { CollectionSlug } from "payload";
import { Button } from '@/components/ui/button'
export function SignOutButton({
userCollectionSlug = "users",
@@ -8,7 +9,7 @@ export function SignOutButton({
userCollectionSlug?: CollectionSlug;
}) {
return (
<button
<Button
type="button"
onClick={async () => {
await fetch(`/api/${userCollectionSlug}/logout`, {
@@ -21,6 +22,6 @@ export function SignOutButton({
}}
>
Sign Out
</button>
</Button>
);
}

View File

@@ -1 +1,124 @@
@import "tailwindcss";
@import "tw-animate-css";
@theme {
--color-htl-red: #e4534dff;
}
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 116.6 137.2" style="enable-background:new 0 0 116.6 137.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#E4534D;}
.st2{fill:#3D4C5A;}
</style>
<g>
<g>
<path class="st0" d="M111.4,0H5.1C2.3,0,0,2.3,0,5.1v127c0,2.8,2.3,5.1,5.1,5.1h106.3c2.8,0,5.1-2.3,5.1-5.1V5.1
C116.6,2.3,114.2,0,111.4,0"/>
<path class="st1" d="M109.2,100.6c0,2.5-2,4.5-4.5,4.5H12c-2.5,0-4.5-2-4.5-4.5V11.7c0-2.5,2-4.5,4.5-4.5h92.8
c2.5,0,4.5,2,4.5,4.5V100.6z"/>
</g>
<polygon class="st0" points="26.8,81.3 20.3,81.3 20.3,70.1 15.1,70.1 15.1,97.5 20.3,97.5 20.3,85.9 26.8,85.9 26.8,97.5 32,97.5
32,70.1 26.8,70.1 "/>
<polygon class="st0" points="35.5,74.7 40.8,74.7 40.8,97.5 45.9,97.5 45.9,74.7 51.2,74.7 51.2,70.1 35.5,70.1 "/>
<polygon class="st0" points="59.9,70.1 54.8,70.1 54.8,97.5 68.9,97.5 68.9,92.8 59.9,92.8 "/>
<g>
<path class="st2" d="M69.1,126.3h-1.9v-5.1h1.8c2,0,2.9,0.8,2.9,2.6C71.8,125.4,70.8,126.3,69.1,126.3 M67.2,114.9h1.9
c1.5,0,2.3,0.6,2.3,1.9c0,1.2-0.8,1.9-2.2,1.9h-2V114.9z M72.6,120.2l-0.5-0.2l0.5-0.3c1.2-0.6,1.9-1.7,1.9-3
c0-2.7-2.1-4.4-5.4-4.4h-4.9v16.5h4.7c4,0,6.1-1.8,6.1-5.1C74.9,122.2,74.1,120.9,72.6,120.2"/>
<path class="st2" d="M27.2,126.3c-2,0-2.9-1.9-2.9-5.7c0-2.4,0.3-5.7,2.9-5.7c1.9,0,2.9,1.9,2.9,5.7
C30,124.5,29.1,126.3,27.2,126.3 M27.2,112.1c-3.8,0-6.1,3.2-6.1,8.5c0,5.4,2.2,8.5,6.1,8.5c3.8,0,6.1-3.2,6.1-8.5
C33.2,115.3,30.9,112.1,27.2,112.1"/>
<path class="st2" d="M41,119.9h-1.7v-4.8h1.6c1.9,0,2.9,0.8,2.9,2.4C43.7,118.6,43.2,119.9,41,119.9 M46.8,117.5
c0-3.2-2.1-5.1-5.8-5.1h-4.8v16.5h3.1v-6.3h1.2l2.9,6.3h3.4l-3.2-6.9l0.3-0.1C45.2,121.3,46.8,120,46.8,117.5"/>
<path class="st2" d="M57.7,119.2c0,0.7,0,1.7,0.1,2.5l0,0.3l0,1.3l-0.5-1.2c-0.3-0.8-0.8-1.8-1.1-2.5l-3.2-7.3h-3.2v16.5h2.8v-6.9
c0-0.7,0-1.8,0-2.8l0-1.2l0.5,1.1c0.4,1,0.8,2,1,2.4l3.4,7.4h3v-16.5h-2.8V119.2z"/>
<path class="st2" d="M89.6,119.9h-1.7v-4.8h1.6c1.9,0,2.9,0.8,2.9,2.4C92.3,118.6,91.8,119.9,89.6,119.9 M95.4,117.5
c0-3.2-2.1-5.1-5.8-5.1h-4.8v16.5h3.1v-6.3h1.3l2.9,6.3h3.4l-3.2-6.9l0.2-0.1C93.9,121.3,95.4,120,95.4,117.5"/>
<path class="st2" d="M106.3,112.4v6.8c0,0.7,0,1.8,0.1,2.7l0,0.2l0,1.2l-0.5-1.1c-0.3-0.8-0.8-1.8-1.1-2.5l-3.2-7.3h-3.2v16.5h2.8
v-6.9c0-0.8,0-1.8,0-2.8l0-1.3l0.5,1.2c0.4,1,0.8,2,1,2.4l3.4,7.4h3v-16.5H106.3z"/>
</g>
<rect x="77.9" y="112.4" class="st2" width="3.1" height="16.5"/>
<g>
<path class="st2" d="M11.7,126.2h-1v-11.1h1c2.6,0,3.8,1.8,3.8,5.6C15.5,124.5,14.4,126.2,11.7,126.2 M11.7,112.4H7.6v16.5h4
c3.2,0,7.1-1.4,7.1-8.3C18.7,115.2,16.4,112.4,11.7,112.4"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,19 +1,23 @@
import React from 'react'
import './globals.css'
import React from "react";
import "./globals.css";
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: "A blank template using Payload in a Next.js app.",
title: "Payload Blank Template",
};
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
const { children } = props;
return (
<html lang="en">
<body>
<html lang="de-AT">
<body className="w-full min-h-full">
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
)
);
}

View File

@@ -1,18 +1,39 @@
import { auth } from "@/auth";
import { getPayloadSession } from "payload-authjs";
import { SignInButton } from "./_components/SignInButton";
import { SignOutButton } from "./_components/SignOutButton";
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="bg-red-500 bg-">
<h2 className="text-4xl">Mooongo Geiss</h2>
<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>
{payloadSession ? <SignOutButton /> : <SignInButton />}
{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>

View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { getPayload } from 'payload';
import config from '@payload-config';
const payload = await getPayload({ config });
export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
const search = searchParams.get('search');
if (!search) {
return NextResponse.json({ error: 'No documents matching search found' }, { status: 404 });
}
const response = await payload.find({
collection: 'papers',
where: {
or: [
{ title: { contains: search } },
{ issue: { contains: search } },
{ goal: { contains: search } },
{ 'technologies.description': { contains: search } },
{ 'prototype.description': { contains: search } },
{ 'authors.description': { contains: search } },
],
},
});
const titles = response.docs.map((doc) => doc.title);
return NextResponse.json({ titles });
}

View File

@@ -8,7 +8,18 @@ export const Papers: CollectionConfig = {
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
const paper = await findByID({ collection: 'papers', id });
return paper.authors.some(author => author.user === user.id); // Check if the user is an author
},
},
admin: {
useAsTitle: 'title',