first commit
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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))));
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Page() {
|
||||
redirect(`gallery/all/1`);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user