@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const photos = await prisma.photo.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
});
|
||||
return NextResponse.json(photos);
|
||||
} catch (error) {
|
||||
console.error("Photos fetch error:", error);
|
||||
return NextResponse.json([], { status: 200 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const ALLOWED_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
];
|
||||
const MAX_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: "Файл не найден" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Недопустимый тип файла" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return NextResponse.json(
|
||||
{ error: "Файл слишком большой (макс. 10 МБ)" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "jpg";
|
||||
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.${ext}`;
|
||||
const uploadDir = path.join(process.cwd(), "public", "uploads");
|
||||
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
await writeFile(path.join(uploadDir, filename), buffer);
|
||||
|
||||
const url = `/uploads/${filename}`;
|
||||
|
||||
const photo = await prisma.photo.create({
|
||||
data: {
|
||||
filename,
|
||||
originalName: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
url,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(photo);
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Ошибка сервера при загрузке" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-background: #0a0a0f;
|
||||
--color-surface: #12121a;
|
||||
--color-surface-hover: #1a1a26;
|
||||
--color-border: #2a2a3a;
|
||||
--color-muted: #71717a;
|
||||
--color-foreground: #fafafa;
|
||||
--color-accent: #6366f1;
|
||||
--color-accent-light: #818cf8;
|
||||
--color-accent-glow: rgba(99, 102, 241, 0.4);
|
||||
--color-success: #22c55e;
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.gradient-text {
|
||||
@apply bg-gradient-to-r from-indigo-400 via-violet-400 to-purple-400 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@apply bg-surface/60 backdrop-blur-xl border border-border/50;
|
||||
}
|
||||
|
||||
.glow {
|
||||
box-shadow: 0 0 60px -12px var(--color-accent-glow);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PhotoHost — Быстрый фотохостинг",
|
||||
description:
|
||||
"Загружайте, храните и делитесь фотографиями мгновенно. Бесплатный современный фотохостинг.",
|
||||
icons: { icon: "/icon.svg" },
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="ru">
|
||||
<body className={`${inter.variable} min-h-screen`}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Header } from "@/components/Header";
|
||||
import { Hero } from "@/components/Hero";
|
||||
import { UploadZone } from "@/components/UploadZone";
|
||||
import { Gallery } from "@/components/Gallery";
|
||||
import { Features } from "@/components/Features";
|
||||
import { Footer } from "@/components/Footer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
async function getRecentPhotos() {
|
||||
try {
|
||||
return await prisma.photo.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 12,
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const photos = await getRecentPhotos();
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
<div className="pointer-events-none fixed inset-0 -z-10">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 h-[600px] w-[800px] rounded-full bg-indigo-600/10 blur-[120px]" />
|
||||
<div className="absolute bottom-0 right-0 h-[400px] w-[600px] rounded-full bg-violet-600/8 blur-[100px]" />
|
||||
<div className="absolute top-1/3 left-0 h-[300px] w-[400px] rounded-full bg-purple-600/6 blur-[80px]" />
|
||||
</div>
|
||||
|
||||
<Header />
|
||||
<main>
|
||||
<Hero />
|
||||
<UploadZone />
|
||||
<Gallery initialPhotos={photos} />
|
||||
<Features />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user