first commit

This commit is contained in:
DerTyp7
2025-10-06 19:22:47 +02:00
commit aecc4cf549
75 changed files with 9303 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
import ImageManager from '@/components/ImageManager';
import ImageUpload from '@/components/ImageUpload';
import { ImagesResponse, TagsResponse } from '@/interfaces/api';
import { getAuthStatus } from '@/lib/auth-utils';
import styles from '@/styles/AdminPage.module.scss';
import { redirect } from 'next/navigation';
export default async function AdminPage() {
const { isAuthenticated } = await getAuthStatus();
if (!isAuthenticated) {
redirect('/login');
}
const fetchImages = async () => {
const apiUrl = new URL(
'/api/images?imagesPerPage=-1}',
process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
);
const response = await fetch(apiUrl.toString());
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return (await response.json()) as ImagesResponse;
};
const fetchTags = async () => {
const apiUrl = new URL('/api/tags', process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000');
const response = await fetch(apiUrl.toString());
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return (await response.json()) as TagsResponse;
};
return (
<div className={styles.container}>
<ImageUpload />
<ImageManager tags={await fetchTags()} images={(await fetchImages()).images} />
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { isAuthenticated } from '@/lib/auth-utils';
import { deleteImageById } from '@/lib/data';
import { NextRequest, NextResponse } from 'next/server';
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await context.params;
const status = await deleteImageById(id);
if (status === 0) {
return NextResponse.json({ message: 'File deleted successfully' }, { status: 201 });
} else {
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+101
View File
@@ -0,0 +1,101 @@
import { imagesDir } from '@/const/api';
import { ImagesResponse } from '@/interfaces/api';
import { ImageMeta } from '@/interfaces/image';
import { isAuthenticated } from '@/lib/auth-utils';
import { addImage, getImageData, stringToTags } from '@/lib/data';
import { promises as fs } from 'fs';
import { NextRequest, NextResponse } from 'next/server';
import sharp from 'sharp';
import { v4 as uuidv4 } from 'uuid';
export async function GET(request: NextRequest) {
try {
const tag = request.nextUrl.searchParams.get('tag');
const page = request.nextUrl.searchParams.get('page');
const imagesPerPageParam = request.nextUrl.searchParams.get('imagesPerPage');
let imagesPerPage = imagesPerPageParam !== null ? +imagesPerPageParam : 20;
const images: ImageMeta[] = await getImageData();
let responseImages = images;
if (tag) {
responseImages = responseImages.filter((image: ImageMeta) => image.tags.includes(tag));
}
if (imagesPerPage === -1) {
imagesPerPage = responseImages.length;
}
const currentPage = page ? parseInt(page, 10) : 1;
const startIndex = (currentPage - 1) * imagesPerPage;
const endIndex = startIndex + imagesPerPage;
const totalPages = Math.ceil(responseImages.length / imagesPerPage);
responseImages = responseImages.slice(
startIndex,
endIndex < responseImages.length ? endIndex : responseImages.length,
);
return NextResponse.json(
{
images: responseImages,
page: currentPage,
totalPages: totalPages,
} as ImagesResponse,
{ status: 200 },
);
} catch (error) {
console.error('Error reading images data:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const tags = formData.get('tags') as string;
if (!file) {
return NextResponse.json({ error: 'No files received.' }, { status: 400 });
}
if (!file.type.startsWith('image/')) {
return NextResponse.json({ error: 'Only image files are allowed.' }, { status: 400 });
}
const uuid = uuidv4();
const fileExtension = file.name.split('.').pop();
const uuidFilename = `${uuid}.${fileExtension}`;
const relativePath = `${uuidFilename}`;
const buffer = Buffer.from(await file.arrayBuffer());
try {
await fs.access(imagesDir);
} catch {
await fs.mkdir(imagesDir, { recursive: true });
}
const imageInfo = await sharp(buffer).metadata();
const newImage: ImageMeta = {
id: uuid,
relative_path: relativePath,
tags: stringToTags(tags),
aspect_ratio: imageInfo.width && imageInfo.height ? imageInfo.width / imageInfo.height : 1,
width: imageInfo.width || 0,
height: imageInfo.height || 0,
};
await addImage(newImage);
return NextResponse.json({ message: 'File deleted successfully' }, { status: 201 });
} catch (error) {
console.error(error);
}
}
+21
View File
@@ -0,0 +1,21 @@
import { isAuthenticated } from '@/lib/auth-utils';
import { stringToTags, updateTagsOfImageId } from '@/lib/data';
import { NextRequest, NextResponse } from 'next/server';
export async function PUT(request: NextRequest, context: { params: Promise<{ imageId: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { imageId } = await context.params;
const formData = await request.formData();
const tags = stringToTags(formData.get('tags')?.toString() ?? '');
const status = await updateTagsOfImageId(imageId, tags ?? []);
if (status === 0) {
return NextResponse.json({ message: 'Tags updated successfully', tags: tags }, { status: 201 });
} else {
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+8
View File
@@ -0,0 +1,8 @@
import { ImageMeta } from '@/interfaces/image';
import { getImageData } from '@/lib/data';
import { NextResponse } from 'next/server';
export async function GET() {
const data: ImageMeta[] = await getImageData();
return NextResponse.json(Array.from(new Set(data.flatMap((image) => image.tags))));
}
+52
View File
@@ -0,0 +1,52 @@
import Gallery from '@/components/Gallery';
import Topbar from '@/components/Topbar';
import { ImagesResponse, TagsResponse } from '@/interfaces/api';
import { redirect } from 'next/navigation';
import { Suspense } from 'react';
export default async function GalleryPage({ params }: { params: Promise<{ page: number; tag: string }> }) {
const { tag, page } = await params;
const fetchImages = async () => {
const apiUrl = new URL(
`/api/images?page=${page}&tag=${tag === 'all' ? '' : tag}`,
process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
);
const response = await fetch(apiUrl.toString());
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return (await response.json()) as ImagesResponse;
};
const fetchTags = async () => {
const apiUrl = new URL('/api/tags', process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000');
const response = await fetch(apiUrl.toString());
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return (await response.json()) as TagsResponse;
};
const images = await fetchImages();
const tags = await fetchTags();
if (images.totalPages < page && images.totalPages > 0) {
redirect(`/gallery/${tag}/${images.totalPages}`);
}
if (page <= 0) {
redirect(`/gallery/${tag}/1`);
}
return (
<>
<Suspense fallback={<div>Loading gallery...</div>}>
<Topbar activeTag={tag} tags={tags} page={page} totalPages={images.totalPages} />
<Gallery initialImages={images.images} />
</Suspense>
</>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default async function Page({ params }: { params: Promise<{ page: number; tag: string }> }) {
redirect(`gallery/${(await params).tag}/1`);
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default async function Page() {
redirect(`gallery/all/1`);
}
+10
View File
@@ -0,0 +1,10 @@
import Imprint from '@/components/Imprint';
import styles from './styles.module.scss';
export default function ImprintPage() {
return (
<div className={styles.container}>
<Imprint />
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
.container {
display: flex;
justify-content: center;
flex: 1;
}
+39
View File
@@ -0,0 +1,39 @@
* {
--header-height: 50px;
--color-accent: #3cdbc0;
}
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: rgb(27, 27, 27);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body,
#root {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: 1px;
min-height: 100vh;
scrollbar-gutter: stable;
background: linear-gradient(-120deg, rgba(0, 0, 0, 0), #06565a49);
}
a {
text-decoration: none;
color: inherit;
}
+22
View File
@@ -0,0 +1,22 @@
import Footer from '@/components/Footer';
import Header from '@/components/Header';
import { ConfigProvider } from '@/contexts/ConfigContext';
import './index.scss';
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<ConfigProvider>
<body>
<Header />
{children}
<Footer currentYear={new Date().getFullYear()} />
</body>
</ConfigProvider>
</html>
);
}
+13
View File
@@ -0,0 +1,13 @@
import LoginForm from '@/components/LoginForm';
import { getAuthStatus } from '@/lib/auth-utils';
import { redirect } from 'next/navigation';
export default async function LoginPage() {
const { isAuthenticated } = await getAuthStatus();
if (isAuthenticated) {
redirect('/admin');
}
return <LoginForm />;
}
+26
View File
@@ -0,0 +1,26 @@
'use client';
import { useConfig } from '@/contexts/configExports';
import Link from 'next/link';
import styles from './styles.module.scss';
export default function HomePage() {
const { config } = useConfig();
return (
<>
<div className={styles.home}>
<div className={styles.homeText}>
<h1
className={styles.homeTextHeadline}
dangerouslySetInnerHTML={{ __html: config?.home.headline || '' }}></h1>
<p className={styles.homeTextParagraph} dangerouslySetInnerHTML={{ __html: config?.home.text || '' }}></p>
<Link
href="/gallery"
className={styles.homeButton}
dangerouslySetInnerHTML={{ __html: config?.home.buttonText || '' }}></Link>
</div>
</div>
</>
);
}
+63
View File
@@ -0,0 +1,63 @@
.home {
display: flex;
flex: 1;
padding: 0 40px;
overflow: hidden;
background-image: url('/landing-page.jpg');
background-size: cover;
@media (max-width: 1200px) {
justify-content: center;
}
}
.homeText {
max-width: 750px;
min-width: 400px;
height: 100%;
margin-top: 80px;
padding: 0 10px;
z-index: 1;
@media (min-width: 1200px) {
margin-left: 100px;
}
&Headline {
letter-spacing: 5px;
text-shadow: 0px 0px #fff;
color: #fff;
}
&Paragraph {
letter-spacing: 2px;
text-shadow: 0px 0px #fff;
color: #fff;
margin-bottom: 50px;
b {
color: var(--color-accent);
}
}
}
.homeButton {
display: inline-block;
padding: 15px 30px;
background-color: rgba(165, 165, 165, 0.062);
border: 3px solid #afafaf;
border-radius: 5px;
font-size: 11pt;
font-weight: bold;
letter-spacing: 1.5px;
cursor: pointer;
transition: all 50ms linear;
text-decoration: none;
color: inherit;
&:hover {
border-color: var(--color-accent);
color: var(--color-accent);
background-color: rgba(165, 165, 165, 0.15);
}
}