first commit

This commit is contained in:
2026-06-17 15:58:02 +03:00
commit 6b15d70955
45 changed files with 11598 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
.next
node_modules
.env
.env.*.local
.git
.vscode
docs
README.md
AGENTS.md
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
/docs
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+42
View File
@@ -0,0 +1,42 @@
# AGENTS.md
## Stack
- Framework: Next.js 16, App Router, TypeScript strict
- Styling: Tailwind CSS v4
- UI: shadcn/ui (new-york style, OKLCH)
- Animation: Framer Motion
- Icons: Lucide React
- i18n: next-intl
- ORM: Prisma + PostgreSQL
- Auth: NextAuth.js v5
- Media: Cloudinary
- Deploy: Coolify (Docker, standalone output)
## Sabit Tercihler
- Mock data: USE_MOCK=true (demo aşaması)
- proxy.ts kullan — middleware.ts deprecated (Next.js 15.3+)
- İletişim formu sadece /iletisim sayfasında — ana sayfada olmaz
- Footer'da "Created by ayris.tech" linki zorunlu
- Dockerfile'da dummy DATABASE_URL (prisma generate için)
## Altyapı
- Gitea: https://git.ayris.tech (kullanıcı: ayrisdev)
- Coolify: https://client2.ayris.tech
- Cloudflare zone: ayris.tech
- Server IP: 188.245.175.169
## docs/ Klasörü
- docs/prd.md → ana içerik kaynağı
- docs/*.html → varsa mevcut site içeriği
- docs/*.md → ek belgeler
## Aktif Skill'ler
- nextjs-seo → sitemap, metadata, robots.txt
- next-best-practices → kod kalitesi
- nextjs-app-router-patterns → Server Actions, Suspense
- demo-site → komple site üretimi
- design-demo → görsel kalite
- coolify-deploy → deploy pipeline
## Proje Özel Notlar
<!-- Buraya proje bazlı notlar ekle -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+34
View File
@@ -0,0 +1,34 @@
FROM node:22-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --legacy-peer-deps
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
# Prisma generate için dummy URL — build sırasında gerçek DB gerekmez
ARG DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy
ENV DATABASE_URL=$DATABASE_URL
RUN npx prisma generate
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next && chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+97
View File
@@ -0,0 +1,97 @@
'use client'
import { signOut } from 'next-auth/react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, Users, Settings, LogOut, Menu, X } from 'lucide-react'
import { useState } from 'react'
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const [sidebarOpen, setSidebarOpen] = useState(false)
const navigation = [
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
{ name: 'Kullanıcılar', href: '/admin/users', icon: Users },
{ name: 'Ayarlar', href: '/admin/settings', icon: Settings },
]
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-gray-900/80 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<div className={`
fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-950 border-r border-gray-200 dark:border-gray-800
transform transition-transform duration-200 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}>
<div className="h-full flex flex-col">
<div className="h-16 flex items-center px-6 border-b border-gray-200 dark:border-gray-800">
<h1 className="text-lg font-bold text-gray-900 dark:text-white">Admin Paneli</h1>
<button
className="ml-auto lg:hidden text-gray-500"
onClick={() => setSidebarOpen(false)}
>
<X className="h-5 w-5" />
</button>
</div>
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const isActive = pathname.endsWith(item.href)
return (
<Link
key={item.name}
href={item.href}
className={`
flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors
${isActive
? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'}
`}
>
<item.icon className={`mr-3 flex-shrink-0 h-5 w-5 ${isActive ? 'text-gray-900 dark:text-white' : 'text-gray-400'}`} />
{item.name}
</Link>
)
})}
</nav>
<div className="p-4 border-t border-gray-200 dark:border-gray-800">
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="flex w-full items-center px-3 py-2.5 text-sm font-medium text-red-600 dark:text-red-400 rounded-md hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
>
<LogOut className="mr-3 h-5 w-5" />
Çıkış Yap
</button>
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<header className="h-16 flex items-center lg:hidden bg-white dark:bg-gray-950 border-b border-gray-200 dark:border-gray-800 px-4">
<button
onClick={() => setSidebarOpen(true)}
className="text-gray-500 hover:text-gray-900 dark:hover:text-white focus:outline-none"
>
<Menu className="h-6 w-6" />
</button>
<span className="ml-4 text-lg font-bold text-gray-900 dark:text-white">Admin Paneli</span>
</header>
<main className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
{children}
</main>
</div>
</div>
)
}
+82
View File
@@ -0,0 +1,82 @@
import { auth } from '@/lib/auth'
export default async function AdminDashboardPage() {
const session = await auth()
return (
<div className="space-y-8 p-6 md:p-10 max-w-7xl mx-auto min-h-screen">
<div className="flex flex-col gap-2">
<h2 className="text-3xl font-bold tracking-tight text-foreground uppercase">Teras Yönetim Paneli</h2>
<p className="text-muted-foreground text-lg">
Hoş geldiniz, {session?.user?.name || session?.user?.email}. İşte restoranınızın genel görünümü.
</p>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{/* Placeholder Stat Cards */}
{[
{ name: 'Bugünkü Rezervasyonlar', stat: '24', trend: '+4 dünden' },
{ name: 'Aktif Masalar', stat: '12 / 30', trend: '%40 Doluluk' },
{ name: 'Aylık Ziyaretçi', stat: '1,452', trend: '+%12 geçen aya göre' },
{ name: 'Stok: Dry Aged Et', stat: '45 porsiyon', trend: 'Kritik seviyeye yakın' },
].map((item) => (
<div
key={item.name}
className="rounded-lg bg-card border border-border/50 p-6 shadow-sm hover:border-primary/30 transition-colors"
>
<dt className="truncate text-sm font-medium text-muted-foreground uppercase tracking-wider">{item.name}</dt>
<dd className="mt-2 text-4xl font-bold tracking-tight text-primary">
{item.stat}
</dd>
<p className="mt-2 text-xs text-muted-foreground">{item.trend}</p>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="rounded-lg bg-card border border-border/50 shadow-sm">
<div className="p-6 border-b border-border/50">
<h3 className="text-lg font-bold text-foreground uppercase tracking-wider">Son Rezervasyon Talepleri</h3>
</div>
<div className="p-6">
<div className="space-y-4">
{[
{ name: 'Ahmet Yılmaz', time: 'Bu akşam 20:00', size: '4 Kişi', status: 'Onay Bekliyor' },
{ name: 'Ayşe Demir', time: 'Yarın 19:30', size: '2 Kişi', status: 'Onaylandı' },
{ name: 'Mehmet Kaya', time: '18 Haziran 21:00', size: '6 Kişi', status: 'Onaylandı' },
].map((req, i) => (
<div key={i} className="flex justify-between items-center p-4 bg-background rounded border border-border/30">
<div>
<p className="font-bold text-foreground">{req.name}</p>
<p className="text-sm text-muted-foreground">{req.time} {req.size}</p>
</div>
<span className={`text-xs px-2 py-1 rounded-full uppercase tracking-wider font-bold ${req.status === 'Onay Bekliyor' ? 'bg-secondary/20 text-secondary' : 'bg-green-500/20 text-green-500'}`}>
{req.status}
</span>
</div>
))}
</div>
</div>
</div>
<div className="rounded-lg bg-card border border-border/50 shadow-sm">
<div className="p-6 border-b border-border/50">
<h3 className="text-lg font-bold text-foreground uppercase tracking-wider">Sistem & İletişim Bildirimleri</h3>
</div>
<div className="p-6">
<div className="space-y-4">
<div className="p-4 bg-background rounded border border-border/30">
<p className="text-sm text-foreground"><strong>Yeni Mesaj:</strong> Müşteri memnuniyet anketi hakkında (Zeynep Ç.)</p>
<p className="text-xs text-muted-foreground mt-1">2 saat önce</p>
</div>
<div className="p-4 bg-background rounded border border-border/30">
<p className="text-sm text-foreground"><strong>Sistem Uyarı:</strong> Veritabanı yedeği başarıyla alındı.</p>
<p className="text-xs text-muted-foreground mt-1">Dün 03:00</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
+59
View File
@@ -0,0 +1,59 @@
'use client'
import { useTranslations } from 'next-intl'
import { motion } from 'framer-motion'
import Image from 'next/image'
const galleryImages = [
"https://images.unsplash.com/photo-1544025162-d76694265947?q=80&w=800&auto=format&fit=crop",
"https://images.unsplash.com/photo-1555939594-58d7cb561ad1?q=80&w=800&auto=format&fit=crop",
"https://images.unsplash.com/photo-1600891964092-4316c288032e?q=80&w=800&auto=format&fit=crop",
"https://images.unsplash.com/photo-1594041680534-e8c8cdebd659?q=80&w=800&auto=format&fit=crop",
"https://images.unsplash.com/photo-1603048297172-c92544798d5e?q=80&w=800&auto=format&fit=crop",
"https://images.unsplash.com/photo-1514933651103-005eec06c04b?q=80&w=800&auto=format&fit=crop"
]
export default function GalleryPage() {
const nav = useTranslations('nav')
return (
<div className="container mx-auto px-4 py-24 min-h-screen flex flex-col items-center">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1 className="text-4xl md:text-5xl font-bold text-primary mb-4 uppercase tracking-wider">
{nav('gallery')}
</h1>
<p className="text-lg text-muted-foreground">
Ateşin ve lezzetin sanata dönüştüğü anlar.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
{galleryImages.map((src, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5, delay: idx * 0.1 }}
className="aspect-square relative overflow-hidden rounded-lg group"
>
<Image
src={src}
alt={`Gallery Image ${idx + 1}`}
fill
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex items-center justify-center">
<span className="text-white font-bold tracking-wider uppercase">Teras Steakhouse</span>
</div>
</motion.div>
))}
</div>
</div>
)
}
+99
View File
@@ -0,0 +1,99 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { MapPin, Phone, Mail, Send } from 'lucide-react'
export default async function ContactPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('contact')
return (
<div className="container mx-auto px-4 py-24">
<div className="max-w-5xl mx-auto">
<h1 className="text-4xl md:text-5xl font-bold text-primary mb-12 text-center uppercase tracking-wider">
{t('title')}
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
{/* Contact Info */}
<div className="space-y-8 bg-card p-8 rounded-lg border border-border/50">
<h2 className="text-2xl font-bold text-foreground uppercase">Teras Steakhouse</h2>
<div className="space-y-6">
<div className="flex items-start gap-4">
<MapPin className="w-6 h-6 text-primary shrink-0" />
<div>
<h3 className="font-semibold text-foreground">Adres</h3>
<p className="text-muted-foreground mt-1">{t('address')}</p>
</div>
</div>
<div className="flex items-start gap-4">
<Phone className="w-6 h-6 text-primary shrink-0" />
<div>
<h3 className="font-semibold text-foreground">Telefon</h3>
<a href={`tel:${t('phone').replace(/\s/g, '')}`} className="text-muted-foreground hover:text-primary transition-colors mt-1 block">
{t('phone')}
</a>
</div>
</div>
<div className="flex items-start gap-4">
<Mail className="w-6 h-6 text-primary shrink-0" />
<div>
<h3 className="font-semibold text-foreground">E-posta</h3>
<a href={`mailto:${t('email')}`} className="text-muted-foreground hover:text-primary transition-colors mt-1 block">
{t('email')}
</a>
</div>
</div>
</div>
</div>
{/* Contact Form */}
<div className="bg-card p-8 rounded-lg border border-border/50">
<form className="space-y-6" action="#" method="POST">
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium text-foreground">
{t('form_name')}
</label>
<input
id="name"
type="text"
required
className="w-full bg-background border border-border rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground"
/>
</div>
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium text-foreground">
{t('form_email')}
</label>
<input
id="email"
type="email"
required
className="w-full bg-background border border-border rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground"
/>
</div>
<div className="space-y-2">
<label htmlFor="message" className="text-sm font-medium text-foreground">
{t('form_message')}
</label>
<textarea
id="message"
required
rows={5}
className="w-full bg-background border border-border rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary/50 text-foreground resize-none"
/>
</div>
<button
type="submit"
className="w-full bg-primary text-white font-bold uppercase tracking-wider py-4 rounded-md hover:bg-primary/90 transition-colors flex items-center justify-center gap-2"
>
{t('form_submit')}
<Send className="w-4 h-4" />
</button>
</form>
</div>
</div>
</div>
</div>
)
}
+66
View File
@@ -0,0 +1,66 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Teras Steakhouse | Inspired by Open Fire",
description: "Premium steakhouse experience with dry aged meat, argentine grill and fine wines.",
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}));
}
export default async function RootLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!routing.locales.includes(locale as any)) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages();
return (
<html
lang={locale}
className={`dark ${geistSans.variable} ${geistMono.variable} h-full antialiased`}
data-scroll-behavior="smooth"
>
<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Inter:wght@100..900&family=Montserrat:wght@100..900&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" />
</head>
<body className="min-h-full flex flex-col bg-background text-foreground" suppressHydrationWarning>
<NextIntlClientProvider messages={messages}>
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</NextIntlClientProvider>
</body>
</html>
);
}
+96
View File
@@ -0,0 +1,96 @@
'use client'
import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { Flame } from 'lucide-react'
export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
const result = await signIn('credentials', {
redirect: false,
email,
password,
})
if (result?.error) {
setError('Geçersiz e-posta veya şifre')
setLoading(false)
} else {
router.push('/admin')
router.refresh()
}
}
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md bg-card rounded-xl shadow-lg border border-border/50 overflow-hidden">
<div className="p-8">
<div className="text-center mb-8 flex flex-col items-center">
<Flame className="w-12 h-12 text-primary mb-4" />
<h1 className="text-2xl font-bold uppercase tracking-wider text-foreground">Teras Steakhouse</h1>
<p className="text-sm text-muted-foreground mt-2 uppercase tracking-wide">Yönetici Girişi</p>
</div>
{error && (
<div className="bg-destructive/10 text-destructive p-3 rounded-md text-sm mb-6 border border-destructive/20">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium text-foreground mb-1 uppercase tracking-wide" htmlFor="email">
E-posta
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 border border-border rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary/50 bg-background text-foreground transition-colors outline-none"
placeholder="admin@ayris.tech"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1 uppercase tracking-wide" htmlFor="password">
Şifre
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-3 border border-border rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary/50 bg-background text-foreground transition-colors outline-none"
placeholder="••••••••"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-primary hover:bg-primary/90 text-primary-foreground font-bold py-3 px-4 rounded-md transition-colors uppercase tracking-wider disabled:opacity-70 disabled:cursor-not-allowed mt-2"
>
{loading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
</button>
</form>
<div className="mt-8 pt-6 border-t border-border/50 text-center text-xs text-muted-foreground">
Demo: admin@ayris.tech / admin
</div>
</div>
</div>
</div>
)
}
+195
View File
@@ -0,0 +1,195 @@
'use client'
import { useTranslations } from 'next-intl'
import { useEffect } from 'react'
export default function MenuPage() {
const t = useTranslations('menu')
useEffect(() => {
// Intersection Observer to highlight current menu category
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('a[href^="#"]');
const observerOptions = {
root: null,
threshold: 0.5,
rootMargin: '0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute('id');
navLinks.forEach(link => {
if (link.getAttribute('href') === `#${id}`) {
link.classList.add('text-tertiary', 'font-bold');
link.classList.remove('text-on-surface-variant');
} else {
link.classList.remove('text-tertiary', 'font-bold');
link.classList.add('text-on-surface-variant');
}
});
}
});
}, observerOptions);
sections.forEach(section => observer.observe(section));
// Subtle parallax effect on mouse move for cards
const handleMouseMove = (e: MouseEvent) => {
const cards = document.querySelectorAll('.copper-glow');
cards.forEach(card => {
const rect = card.getBoundingClientRect();
const cardX = rect.left + rect.width / 2;
const cardY = rect.top + rect.height / 2;
const angleX = (e.clientY - cardY) / 50;
const angleY = (cardX - e.clientX) / 50;
(card as HTMLElement).style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg)`;
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
}, [])
return (
<main className="relative">
{/* Hero Header */}
<section className="relative h-[614px] flex flex-col items-center justify-center text-center px-[var(--spacing-gutter)] overflow-hidden">
<div className="relative z-10 space-y-4">
<h1 className="font-display-lg text-display-lg-mobile md:text-display-lg italic">{t('title')}</h1>
<div className="w-32 h-px bg-tertiary mx-auto opacity-60"></div>
<p className="font-label-caps text-label-caps text-on-surface-variant tracking-[0.3em] uppercase">{t('subtitle')}</p>
</div>
</section>
{/* Category Navigation (Sticky) */}
<div className="sticky top-[72px] z-40 bg-surface/80 backdrop-blur-xl border-b border-outline-variant/10">
<div className="max-w-[var(--spacing-container-max)] mx-auto px-[var(--spacing-gutter)] overflow-x-auto no-scrollbar">
<div className="flex justify-center gap-8 md:gap-16 py-4 whitespace-nowrap">
<a className="font-label-caps text-[10px] md:text-label-caps text-tertiary font-bold hover:text-tertiary transition-colors uppercase" href="#starters">{t('nav_starters')}</a>
<a className="font-label-caps text-[10px] md:text-label-caps text-on-surface-variant hover:text-tertiary transition-colors uppercase" href="#prime-cuts">{t('nav_prime_cuts')}</a>
<a className="font-label-caps text-[10px] md:text-label-caps text-on-surface-variant hover:text-tertiary transition-colors uppercase" href="#sides">{t('nav_sides')}</a>
<a className="font-label-caps text-[10px] md:text-label-caps text-on-surface-variant hover:text-tertiary transition-colors uppercase" href="#dessert">{t('nav_dessert')}</a>
</div>
</div>
</div>
{/* Menu Sections */}
<div className="max-w-[var(--spacing-container-max)] mx-auto px-[var(--spacing-gutter)] space-y-[var(--spacing-section-padding)] py-[var(--spacing-section-padding)]">
{/* Starters Section */}
<section className="scroll-mt-32" id="starters">
<div className="grid grid-cols-1 md:grid-cols-12 gap-[var(--spacing-gutter)] items-start">
<div className="md:col-span-4 sticky top-48">
<h2 className="font-headline-md text-headline-md mb-4 text-tertiary">{t('starters_title')}</h2>
<p className="font-body-md text-on-surface-variant leading-relaxed">{t('starters_desc')}</p>
<div className="mt-8 h-24 w-px bg-gradient-to-b from-tertiary to-transparent opacity-30 hidden md:block"></div>
</div>
<div className="md:col-span-8 space-y-12">
<div className="group border-b border-outline-variant/10 pb-8 hover:border-tertiary/30 transition-colors duration-500">
<div className="flex justify-between items-baseline mb-2">
<h3 className="font-headline-sm text-headline-sm group-hover:text-tertiary transition-colors">{t('starter1_title')}</h3>
<span className="font-label-caps text-label-caps text-tertiary">22</span>
</div>
<p className="font-body-md text-on-surface-variant">{t('starter1_desc')}</p>
</div>
<div className="group border-b border-outline-variant/10 pb-8 hover:border-tertiary/30 transition-colors duration-500">
<div className="flex justify-between items-baseline mb-2">
<h3 className="font-headline-sm text-headline-sm group-hover:text-tertiary transition-colors">{t('starter2_title')}</h3>
<span className="font-label-caps text-label-caps text-tertiary">34</span>
</div>
<p className="font-body-md text-on-surface-variant">{t('starter2_desc')}</p>
</div>
</div>
</div>
</section>
{/* Featured Highlight - Prime Cuts */}
<section className="scroll-mt-32" id="prime-cuts">
<div className="relative overflow-hidden rounded-lg aspect-[16/9] mb-16">
<img className="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBZQFwOXiMm8B7hPOumV-GiiGFbt3Pl5c6NEOUSC3JFlzzahLgcckvjpa46Q5Ro6XsUyQhroFuxT9OFJEAUt5EpHxEi-C4NXILyPUk-HfKRP2ljE5UAYHdVTAa3q7FP_Tfp0a7ZDvugWHsIjKJBLDDTOC2czJwnC5CvJGL_WYAjtnO5DiNQdQGYe_vDmgTbKnr07TgIu6KUjFnxNr5_3MyOA35vKePtjK51nR_IDbAkZu5GrxfQByM-CE8f8-lPoksDABh3Z2NQD83x" alt="Tomahawk" />
<div className="vignette-overlay-linear absolute inset-0 flex flex-col justify-end p-[var(--spacing-gutter)] md:p-12">
<div className="max-w-xl">
<span className="font-label-caps text-label-caps text-tertiary mb-4 block tracking-widest uppercase">{t('prime_cuts_tag')}</span>
<h2 className="font-display-lg text-display-lg-mobile md:text-headline-md text-white mb-4">{t('prime_cuts_title')}</h2>
<p className="font-body-lg text-on-surface-variant mb-6">{t('prime_cuts_desc')}</p>
<span className="font-label-caps text-label-caps text-tertiary border border-tertiary/40 px-6 py-2 inline-block">{t('mkt_price')}</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--spacing-gutter)]">
<div className="bg-surface-container-low p-8 copper-glow border border-outline-variant/20 hover:border-tertiary/40 transition-all duration-300">
<h3 className="font-headline-sm text-headline-sm mb-2">{t('cut1_title')}</h3>
<p className="font-body-md text-on-surface-variant mb-4">{t('cut1_desc')}</p>
<span className="font-label-caps text-label-caps text-tertiary">58</span>
</div>
<div className="bg-surface-container-low p-8 copper-glow border border-outline-variant/20 hover:border-tertiary/40 transition-all duration-300">
<h3 className="font-headline-sm text-headline-sm mb-2">{t('cut2_title')}</h3>
<p className="font-body-md text-on-surface-variant mb-4">{t('cut2_desc')}</p>
<span className="font-label-caps text-label-caps text-tertiary">145</span>
</div>
<div className="bg-surface-container-low p-8 copper-glow border border-outline-variant/20 hover:border-tertiary/40 transition-all duration-300">
<h3 className="font-headline-sm text-headline-sm mb-2">{t('cut3_title')}</h3>
<p className="font-body-md text-on-surface-variant mb-4">{t('cut3_desc')}</p>
<span className="font-label-caps text-label-caps text-tertiary">74</span>
</div>
</div>
</section>
{/* Sides Section */}
<section className="scroll-mt-32" id="sides">
<div className="section-divider mb-16"></div>
<div className="text-center max-w-2xl mx-auto mb-16">
<h2 className="font-headline-md text-headline-md mb-4">{t('sides_title')}</h2>
<p className="font-body-md text-on-surface-variant">{t('sides_desc')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-16 gap-y-12">
<div className="flex justify-between items-start border-b border-outline-variant/10 pb-4">
<div>
<h4 className="font-label-caps text-label-caps uppercase tracking-wider mb-1">{t('side1_title')}</h4>
<p className="text-sm text-on-surface-variant">{t('side1_desc')}</p>
</div>
<span className="font-label-caps text-label-caps text-tertiary">18</span>
</div>
<div className="flex justify-between items-start border-b border-outline-variant/10 pb-4">
<div>
<h4 className="font-label-caps text-label-caps uppercase tracking-wider mb-1">{t('side2_title')}</h4>
<p className="text-sm text-on-surface-variant">{t('side2_desc')}</p>
</div>
<span className="font-label-caps text-label-caps text-tertiary">16</span>
</div>
</div>
</section>
{/* Dessert Section */}
<section className="scroll-mt-32" id="dessert">
<div className="grid grid-cols-1 md:grid-cols-12 gap-[var(--spacing-gutter)] items-center">
<div className="md:col-span-7 order-2 md:order-1 space-y-12">
<div className="group border-l-2 border-tertiary/20 pl-8 pb-4 hover:border-tertiary transition-colors duration-500">
<h3 className="font-headline-sm text-headline-sm mb-2 group-hover:text-tertiary transition-colors">{t('dessert1_title')}</h3>
<p className="font-body-md text-on-surface-variant mb-2">{t('dessert1_desc')}</p>
<span className="font-label-caps text-label-caps text-tertiary">18</span>
</div>
<div className="group border-l-2 border-tertiary/20 pl-8 pb-4 hover:border-tertiary transition-colors duration-500">
<h3 className="font-headline-sm text-headline-sm mb-2 group-hover:text-tertiary transition-colors">{t('dessert2_title')}</h3>
<p className="font-body-md text-on-surface-variant mb-2">{t('dessert2_desc')}</p>
<span className="font-label-caps text-label-caps text-tertiary">16</span>
</div>
</div>
<div className="md:col-span-5 order-1 md:order-2 mb-8 md:mb-0">
<div className="relative group">
<div className="absolute -inset-4 bg-tertiary/10 blur-xl group-hover:bg-tertiary/20 transition-all duration-700"></div>
<img className="relative z-10 w-full h-80 object-cover rounded-lg" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBPy2xGbUE9MZqJC4F5ZVPD7fP5S1Eklhu3oZjiyREkC4_UyW4G5BxCOwOISwKzdsc3fVI-xIo_4DIV9frboEX0wEkj3mZvUKZBPXgB_I14udTZry1riWVcv1IrgaFtjN3GdtKy8SrIK03SCgZZ4kVzsBH-sxAsRpxBjgy8VU_w_-Ohc5n1x0yjyuNxrRWWM2PbFFA6mFv-4JV6qcV3cA9N-hd_TUVUsXGs9MGfQPNosGHEF_eiOd-Wecb6sNAUoiH_Rl0gl1l7IZLF" alt="Chocolate Fondant" />
<h2 className="font-headline-md text-headline-md mt-6 text-tertiary italic">{t('dessert_title')}</h2>
</div>
</div>
</div>
</section>
</div>
</main>
)
}
+193
View File
@@ -0,0 +1,193 @@
'use client'
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/routing'
import { useEffect } from 'react'
export default function HomePage() {
const t = useTranslations('home')
useEffect(() => {
// Micro-interaction for buttons
const buttons = document.querySelectorAll('button');
buttons.forEach(btn => {
btn.addEventListener('mousedown', () => btn.classList.add('scale-95'));
btn.addEventListener('mouseup', () => btn.classList.remove('scale-95'));
btn.addEventListener('mouseleave', () => btn.classList.remove('scale-95'));
});
// Simple Fade In on Scroll
const observerOptions = { threshold: 0.1 };
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('opacity-100');
entry.target.classList.remove('translate-y-10');
}
});
}, observerOptions);
const sections = document.querySelectorAll('section:not(:first-child)');
sections.forEach(section => {
section.classList.add('transition-all', 'duration-1000', 'translate-y-10', 'opacity-0');
observer.observe(section);
});
}, [])
return (
<main>
{/* Hero Section */}
<section className="relative h-screen flex items-center justify-center overflow-hidden transition-all duration-1000">
<div className="absolute inset-0 z-0">
<img className="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuALRLjbZvJQRlnpOqK0FJXRpeoI4Q_PObykBtlxjxHFCLy_rpnzaUpvICS9bCJFLUNurkL6BnQYbkxPxHITsbreYTPJvVgybXvzTDIN37BpLrQBD34WDr81Es58HM112IRETzzryapmv-CURUfWMFTj5MblCZ9CUmI6LA5BaCnxiQFBgHkK6i09sV42FFOXuVMBMcyufKjkM2vStSHFYu4inukZsoPWxh8ydf9WnkTg0TYEkypsIK4T8LqZoGNav8jPB7R6aMdV6D01" alt="Hero Steak" />
<div className="absolute inset-0 chiaroscuro-gradient"></div>
<div className="absolute inset-0 vignette-overlay"></div>
</div>
<div className="relative z-10 text-center px-[var(--spacing-gutter)] max-w-4xl">
<p className="font-label-caps text-label-caps text-tertiary mb-4 tracking-[0.3em] uppercase">{t('hero.subtitle')}</p>
<h1 className="font-display-lg text-display-lg-mobile md:text-display-lg text-on-surface mb-6">{t('hero.title')}</h1>
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-2xl mx-auto mb-10">
{t('hero.desc')}
</p>
<div className="flex flex-col md:flex-row gap-4 justify-center">
<Link href="/menu" className="bg-secondary-container text-white px-10 py-4 font-label-caps text-label-caps border border-transparent copper-glow transition-all uppercase inline-block">{t('hero.explore_menu')}</Link>
<Link href="#philosophy" className="bg-transparent text-secondary border border-secondary/50 px-10 py-4 font-label-caps text-label-caps hover:bg-secondary/10 transition-all uppercase inline-block">{t('hero.our_philosophy')}</Link>
</div>
</div>
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2">
<span className="font-label-caps text-[10px] text-outline tracking-widest uppercase">Scroll</span>
<div className="w-[1px] h-12 bg-gradient-to-b from-tertiary to-transparent"></div>
</div>
</section>
{/* The Ignis Philosophy */}
<section id="philosophy" className="py-[var(--spacing-section-padding)] bg-surface-container-lowest relative scroll-mt-20">
<div className="max-w-[var(--spacing-container-max)] mx-auto px-[var(--spacing-gutter)] grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
<div className="md:col-span-5 relative group">
<div className="aspect-[4/5] overflow-hidden border border-outline-variant/20">
<img className="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700" src="https://static.wixstatic.com/media/19c276_8160f1c03e0343f1a2c4f456157a74e2~mv2.jpg/v1/fill/w_327,h_479,al_c,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/19c276_8160f1c03e0343f1a2c4f456157a74e2~mv2.jpg" alt="Aged Oak" />
</div>
<div className="absolute -bottom-6 -right-6 w-32 h-32 bg-surface-container border border-outline-variant/30 hidden md:flex items-center justify-center p-4">
<p className="font-label-caps text-[10px] text-tertiary text-center leading-relaxed">{t('philosophy.badge')}</p>
</div>
</div>
<div className="md:col-span-7 md:pl-12">
<span className="font-label-caps text-label-caps text-tertiary mb-6 block uppercase">{t('philosophy.tag')}</span>
<h2 className="font-headline-md text-headline-md text-on-surface mb-8 max-w-md">{t('philosophy.title')}</h2>
<div className="w-20 h-[1px] bg-tertiary mb-8"></div>
<p className="font-body-lg text-body-lg text-on-surface-variant mb-6">
{t('philosophy.desc1')}
</p>
<p className="font-body-md text-body-md text-outline">
{t('philosophy.desc2')}
</p>
</div>
</div>
</section>
{/* Signature Cuts */}
<section className="py-[var(--spacing-section-padding)] bg-surface">
<div className="max-w-[var(--spacing-container-max)] mx-auto px-[var(--spacing-gutter)]">
<div className="text-center mb-16">
<span className="font-label-caps text-label-caps text-secondary mb-4 block uppercase">{t('cuts.tag')}</span>
<h2 className="font-headline-md text-headline-md text-on-surface">{t('cuts.title')}</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-2 md:row-span-2 group relative overflow-hidden bg-surface-container-high aspect-square md:aspect-auto">
<img className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105" src="https://static.wixstatic.com/media/19c276_ce72add7ebdb407597224e87e838bd70~mv2.jpg/v1/fill/w_327,h_479,al_c,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/19c276_ce72add7ebdb407597224e87e838bd70~mv2.jpg" alt="Tomahawk" />
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent"></div>
<div className="absolute bottom-0 left-0 p-8">
<p className="font-label-caps text-[10px] text-secondary mb-2 uppercase">{t('cuts.dry_aged')}</p>
<h3 className="font-headline-sm text-headline-sm text-on-surface mb-2">{t('cuts.t_bone')}</h3>
<p className="font-body-md text-on-surface-variant opacity-0 group-hover:opacity-100 transition-opacity duration-500">{t('cuts.t_bone_desc')}</p>
</div>
</div>
<div className="group relative overflow-hidden aspect-square bg-surface-container">
<img className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBXylHVJVF2D1Ox5qiz8sQwSkUlOwci8kh62Sv8cdmxzU91j1ML3MymH-aTP2nIbmGZvn0W_AgmZKSfXQLI9YNSaYcaxoTh_VRv8NWU34xPwm3md4_ETDTR09cz2nTIfeBcrapG8PY-alnCnt3xFCrgeo0icFENiC5pSFfLxxvuRs0pnSYQgCNtf9AVc3s3PNJeOUW8hApoGrkjWai33iKnKA-JpBDZ6U-R0mW7AUS3sSZNBGfa98-KqVRswC-DiJI8DMHj1qoZan3C" alt="Tomahawk" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center p-4">
<div className="text-center">
<h4 className="font-headline-sm text-headline-sm text-on-surface">{t('cuts.tomahawk')}</h4>
<span className="font-label-caps text-label-caps text-tertiary">{t('cuts.tomahawk_desc')}</span>
</div>
</div>
</div>
<div className="group relative overflow-hidden aspect-square bg-surface-container">
<img className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDczwEijlcMbRMPQy8Dci6cx4PmffAeGbv0iOvph56bRTXI1t9Y1dReFXXp4whLg-EblfFEBNgDq0XrfPe2N1LGtQemcc5U9QHBJaEVgoGO2VlKW8OmFtK2L9KAuwBtUpGyQXcxm_tRD3PsjMOL3IhwPO2GqqTGD-2rnrZJ1GR0Y8awTQK8PhO114GuvBt9OVWAdz6iRSP4en5lNBJYCKcTGOL5Vj43_tM60tpBCAyfz2igf-RBo63GjkfoWMtGVul77nHFfCUw2rFi" alt="New York" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center p-4">
<div className="text-center">
<h4 className="font-headline-sm text-headline-sm text-on-surface">{t('cuts.new_york')}</h4>
<span className="font-label-caps text-label-caps text-tertiary">{t('cuts.new_york_desc')}</span>
</div>
</div>
</div>
<div className="md:col-span-2 group relative h-64 overflow-hidden bg-surface-container-high">
<div className="absolute inset-0 flex items-center justify-center px-12 z-10 pointer-events-none">
<div className="text-center">
<h3 className="font-headline-sm text-headline-sm text-on-surface mb-2">{t('cuts.dry_aged')}</h3>
<p className="font-body-md text-on-surface-variant">{t('cuts.dry_aged_desc')}</p>
</div>
</div>
<img className="w-full h-full object-cover opacity-40 grayscale group-hover:grayscale-0 group-hover:opacity-60 transition-all duration-700" src="https://static.wixstatic.com/media/19c276_3590e6b88f54446e93c31aeb65933107~mv2.jpg/v1/fill/w_326,h_479,al_b,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/19c276_3590e6b88f54446e93c31aeb65933107~mv2.jpg" alt="Porterhouse" />
</div>
</div>
</div>
</section>
{/* The Cellar */}
<section className="py-[var(--spacing-section-padding)] bg-surface-container-lowest relative overflow-hidden">
<div className="absolute left-1/2 top-0 bottom-0 w-[1px] bg-gradient-to-b from-transparent via-tertiary/20 to-transparent"></div>
<div className="max-w-[var(--spacing-container-max)] mx-auto px-[var(--spacing-gutter)] grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div className="order-2 md:order-1">
<span className="font-label-caps text-label-caps text-secondary-container mb-6 block uppercase">{t('cellar.tag')}</span>
<h2 className="font-display-lg-mobile md:font-display-lg text-display-lg-mobile md:text-display-lg text-on-surface mb-8">{t('cellar.title')}</h2>
<p className="font-body-lg text-body-lg text-on-surface-variant mb-8">
{t('cellar.desc')}
</p>
<ul className="space-y-6 mb-10">
<li className="flex justify-between items-end border-b border-outline-variant/30 pb-2">
<div>
<h4 className="font-headline-sm text-[18px] text-on-surface">{t('cellar.wine1_name')}</h4>
<p className="font-body-md text-outline text-sm">{t('cellar.wine1_desc')}</p>
</div>
</li>
<li className="flex justify-between items-end border-b border-outline-variant/30 pb-2">
<div>
<h4 className="font-headline-sm text-[18px] text-on-surface">{t('cellar.wine2_name')}</h4>
<p className="font-body-md text-outline text-sm">{t('cellar.wine2_desc')}</p>
</div>
</li>
<li className="flex justify-between items-end border-b border-outline-variant/30 pb-2">
<div>
<h4 className="font-headline-sm text-[18px] text-on-surface">{t('cellar.wine3_name')}</h4>
<p className="font-body-md text-outline text-sm">{t('cellar.wine3_desc')}</p>
</div>
</li>
</ul>
<Link href="/menu" className="font-label-caps text-label-caps text-secondary flex items-center gap-2 group uppercase">
{t('cellar.view_list')}
<span className="material-symbols-outlined group-hover:translate-x-2 transition-transform">arrow_forward</span>
</Link>
</div>
<div className="order-1 md:order-2 relative">
<div className="aspect-[3/4] bg-surface-container relative">
<img className="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAjziyIdiB8l1fAyEZhCWldYGlClbeZAPQqY86so0hVbUW93mJpPvRcV1kmn2tAP0w8c2nm8MyBBoyJ-7GOU5WchtcLZCv33NWfNevgZqm0wX4nISlnjBN1RmdVBaUfhlyc84XquNYkm1jcA8247tT3HX0x0vkCS5rHzyGl3alUyHWFqIn0awR0s5tzCZ_PBX8X7XlUfY5umq4CGgIUY7_1ly9LIx3ZaYO6p9CgXGisNX3-rEJf0kQjBVPHRzpFsEDFjkatn8WvsoZP" alt="Wine Cellar" />
<div className="absolute -top-10 -left-10 w-40 h-40 border border-secondary/20 -z-10"></div>
<div className="absolute -bottom-10 -right-10 w-40 h-40 border border-secondary/20 -z-10"></div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24 bg-surface text-center">
<div className="max-w-2xl mx-auto px-[var(--spacing-gutter)]">
<h2 className="font-headline-md text-headline-md text-on-surface mb-8">{t('cta.title')}</h2>
<p className="font-body-md text-on-surface-variant mb-12">{t('cta.desc')}</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/rezervasyon" className="bg-secondary-container text-white px-8 py-3 font-label-caps text-label-caps hover:bg-secondary-container/80 transition-all uppercase">{t('cta.button')}</Link>
</div>
</div>
</section>
</main>
)
}
+185
View File
@@ -0,0 +1,185 @@
'use client'
import { useTranslations } from 'next-intl'
import { useState, useEffect } from 'react'
export default function ReservationPage() {
const t = useTranslations('reservation')
const [isSubmitted, setIsSubmitted] = useState(false)
const [formData, setFormData] = useState({
date: '',
time: '',
guests: '2',
occasion: '',
requests: ''
})
useEffect(() => {
// Parallax effect for hero
const handleScroll = () => {
const scrolled = window.pageYOffset;
const heroImage = document.querySelector('.hero-parallax') as HTMLElement;
if (heroImage) {
heroImage.style.transform = `scale(1.05) translateY(${scrolled * 0.3}px)`;
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitted(true)
}
return (
<main>
{/* Hero Section */}
<section className="relative h-[1024px] flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<img alt="Teras Dining Room" className="hero-parallax w-full h-full object-cover blur-[4px] brightness-[0.3] scale-105 transition-transform" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAXZYL1k1MqQOOTVS2XFNUlHm_ujv_2XGjXeqDdqmW2cAzGhZ4YKMmCEyrzgsxhvecujJ71E-2WyhGynN2dOx7cTZN5kWr0OZHk1YS922FdQrqWKJqnyutqRVAiqO4lSs7jQM_4hVi9a2o4y6rFBXIlTFyHZlUWudF1sTQtjjBmk1Nbdr-YCx5t_lcmRArh0U9Q9y3J3wMuZS-Zl-rNtMzgXSyEnwtoLSPzaSFugpPM3nmzszmAAiAdhDXRJZzycQCK8oPbMuQ5JXqp" />
<div className="absolute inset-0 chiaroscuro-gradient"></div>
</div>
{/* Booking Widget */}
<div className="relative z-10 w-full max-w-2xl px-6" id="booking-container">
{!isSubmitted ? (
<div className="booking-card bg-surface-container/60 p-8 md:p-12 shadow-2xl transition-all duration-700 transform opacity-100 translate-y-0" id="reservation-form-wrapper">
<div className="text-center mb-10">
<span className="font-label-caps text-label-caps text-tertiary mb-4 block">{t('subtitle')}</span>
<h1 className="font-headline-md text-headline-md text-on-surface">{t('title')}</h1>
<div className="w-12 h-px bg-on-tertiary-container mx-auto mt-6"></div>
</div>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Date */}
<div className="space-y-2 copper-glow p-0.5 border-b border-outline-variant transition-all duration-300">
<label className="font-label-caps text-[10px] text-on-surface-variant">{t('date')}</label>
<input
type="date"
required
className="w-full bg-transparent border-none focus:ring-0 text-on-surface font-body-md p-0 pb-2"
value={formData.date}
onChange={e => setFormData({...formData, date: e.target.value})}
/>
</div>
{/* Time */}
<div className="space-y-2 copper-glow p-0.5 border-b border-outline-variant transition-all duration-300">
<label className="font-label-caps text-[10px] text-on-surface-variant">{t('time')}</label>
<input
type="time"
required
className="w-full bg-transparent border-none focus:ring-0 text-on-surface font-body-md p-0 pb-2"
value={formData.time}
onChange={e => setFormData({...formData, time: e.target.value})}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Guests */}
<div className="space-y-2 copper-glow p-0.5 border-b border-outline-variant transition-all duration-300">
<label className="font-label-caps text-[10px] text-on-surface-variant">{t('guests')}</label>
<select
required
className="w-full bg-transparent border-none focus:ring-0 text-on-surface font-body-md p-0 pb-2 appearance-none"
value={formData.guests}
onChange={e => setFormData({...formData, guests: e.target.value})}
>
<option className="bg-surface" value="1">1 {t('person')}</option>
<option className="bg-surface" value="2">2 {t('person')}</option>
<option className="bg-surface" value="3">3 {t('person')}</option>
<option className="bg-surface" value="4">4 {t('person')}</option>
<option className="bg-surface" value="5+">5+ {t('person')}</option>
</select>
</div>
{/* Occasion */}
<div className="space-y-2 copper-glow p-0.5 border-b border-outline-variant transition-all duration-300">
<label className="font-label-caps text-[10px] text-on-surface-variant">{t('occasion')}</label>
<select
className="w-full bg-transparent border-none focus:ring-0 text-on-surface font-body-md p-0 pb-2 appearance-none"
value={formData.occasion}
onChange={e => setFormData({...formData, occasion: e.target.value})}
>
<option className="bg-surface" value="">{t('occasion_none')}</option>
<option className="bg-surface" value="birthday">{t('occasion_birthday')}</option>
<option className="bg-surface" value="anniversary">{t('occasion_anniversary')}</option>
<option className="bg-surface" value="business">{t('occasion_business')}</option>
</select>
</div>
</div>
{/* Special Requests */}
<div className="space-y-2 copper-glow p-0.5 border-b border-outline-variant transition-all duration-300">
<label className="font-label-caps text-[10px] text-on-surface-variant">{t('requests')}</label>
<textarea
rows={2}
className="w-full bg-transparent border-none focus:ring-0 text-on-surface font-body-md p-0 pb-2 resize-none placeholder:text-outline-variant"
placeholder={t('requests_placeholder')}
value={formData.requests}
onChange={e => setFormData({...formData, requests: e.target.value})}
></textarea>
</div>
<button type="submit" className="w-full bg-secondary-container text-on-secondary-container py-4 font-label-caps text-label-caps tracking-[0.2em] hover:brightness-110 transition-all duration-300 shadow-lg mt-8 active:scale-[0.98]">
{t('submit')}
</button>
</form>
</div>
) : (
<div className="booking-card bg-surface-container/80 p-12 text-center shadow-2xl transition-all duration-700 opacity-100 transform translate-y-0" id="success-state">
<div className="mb-8">
<span className="material-symbols-outlined text-tertiary text-6xl" style={{ fontVariationSettings: "'wght' 200" }}>check_circle</span>
</div>
<h2 className="font-headline-md text-headline-md text-on-surface mb-4">{t('success_title')}</h2>
<p className="text-on-surface-variant font-body-md mb-10 max-w-sm mx-auto">
{t('success_desc')}
</p>
<div className="flex flex-col gap-4 max-w-xs mx-auto">
<div className="flex justify-between border-b border-outline-variant/30 pb-2">
<span className="font-label-caps text-[10px] text-on-surface-variant">{t('guests')}</span>
<span className="font-body-md text-tertiary">{formData.guests} {t('person')}</span>
</div>
<div className="flex justify-between border-b border-outline-variant/30 pb-2">
<span className="font-label-caps text-[10px] text-on-surface-variant">{t('time')}</span>
<span className="font-body-md text-tertiary">{formData.time || '19:30'}</span>
</div>
</div>
<button onClick={() => setIsSubmitted(false)} className="mt-12 text-secondary font-label-caps text-label-caps border-b border-secondary pb-1 hover:text-tertiary hover:border-tertiary transition-colors">
{t('book_another')}
</button>
</div>
)}
</div>
{/* Scroll Indicator */}
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 flex flex-col items-center gap-4">
<div className="w-px h-16 bg-gradient-to-b from-transparent via-on-tertiary-container to-transparent"></div>
</div>
</section>
{/* Informational Section */}
<section className="bg-background py-[var(--spacing-section-padding)] px-[var(--spacing-gutter)]">
<div className="max-w-[var(--spacing-container-max)] mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div className="space-y-4">
<span className="material-symbols-outlined text-tertiary text-4xl">local_fire_department</span>
<h3 className="font-headline-sm text-headline-sm text-on-surface">{t('info_title1')}</h3>
<p className="text-on-surface-variant text-body-md">{t('info_desc1')}</p>
</div>
<div className="space-y-4">
<span className="material-symbols-outlined text-tertiary text-4xl">restaurant_menu</span>
<h3 className="font-headline-sm text-headline-sm text-on-surface">{t('info_title2')}</h3>
<p className="text-on-surface-variant text-body-md">{t('info_desc2')}</p>
</div>
<div className="space-y-4">
<span className="material-symbols-outlined text-tertiary text-4xl">wine_bar</span>
<h3 className="font-headline-sm text-headline-sm text-on-surface">{t('info_title3')}</h3>
<p className="text-on-surface-variant text-body-md">{t('info_desc3')}</p>
</div>
</div>
</section>
</main>
)
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+154
View File
@@ -0,0 +1,154 @@
@import "tailwindcss";
@theme {
--color-surface-bright: #3a3939;
--color-on-tertiary-fixed: #2e1500;
--color-surface: #141313;
--color-primary-container: #121212;
--color-surface-variant: #353434;
--color-outline-variant: #444748;
--color-on-surface-variant: #c4c7c7;
--color-surface-container-highest: #353434;
--color-surface-container: #201f1f;
--color-on-error: #690005;
--color-on-primary-fixed: #1c1b1b;
--color-tertiary-container: #200d00;
--color-inverse-on-surface: #313030;
--color-on-tertiary-container: #b16d2e;
--color-error: #ffb4ab;
--color-on-primary-container: #7e7d7d;
--color-on-background: #e5e2e1;
--color-tertiary-fixed-dim: #ffb77b;
--color-surface-container-lowest: #0e0e0e;
--color-tertiary: #ffb77b;
--color-on-primary-fixed-variant: #474646;
--color-on-surface: #e5e2e1;
--color-secondary-container: #7a322f;
--color-primary-fixed-dim: #c8c6c5;
--color-outline: #8e9192;
--color-surface-container-high: #2b2a2a;
--color-on-secondary-fixed-variant: #77302d;
--color-on-tertiary-fixed-variant: #6d3a00;
--color-primary: #c8c6c5;
--color-secondary: #ffb3ad;
--color-on-tertiary: #4d2700;
--color-error-container: #93000a;
--color-tertiary-fixed: #ffdcc2;
--color-inverse-surface: #e5e2e1;
--color-secondary-fixed: #ffdad7;
--color-on-secondary-container: #ff9e97;
--color-on-secondary-fixed: #3d0506;
--color-surface-dim: #141313;
--color-primary-fixed: #e5e2e1;
--color-on-secondary: #5a1a19;
--color-inverse-primary: #5f5e5e;
--color-surface-container-low: #1c1b1b;
--color-on-error-container: #ffdad6;
--color-secondary-fixed-dim: #ffb3ad;
--color-surface-tint: #c8c6c5;
--color-background: #141313;
--color-on-primary: #313030;
--radius-DEFAULT: 0.125rem;
--radius-lg: 0.25rem;
--radius-xl: 0.5rem;
--radius-full: 0.75rem;
--spacing-gutter: 24px;
--spacing-unit: 8px;
--spacing-section-padding: 80px;
--spacing-container-max: 1200px;
--spacing-edge-margin-mobile: 20px;
--font-headline-md: "Playfair Display", serif;
--font-body-md: "Inter", sans-serif;
--font-display-lg-mobile: "Playfair Display", serif;
--font-headline-sm: "Playfair Display", serif;
--font-display-lg: "Playfair Display", serif;
--font-body-lg: "Inter", sans-serif;
--font-label-caps: "Montserrat", sans-serif;
}
@layer base {
:root {
--background: var(--color-background);
--foreground: var(--color-on-surface);
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--color-background);
color: var(--color-on-surface);
-webkit-font-smoothing: antialiased;
}
}
@layer utilities {
.font-headline-md { font-family: var(--font-headline-md); }
.font-body-md { font-family: var(--font-body-md); }
.font-display-lg-mobile { font-family: var(--font-display-lg-mobile); }
.font-headline-sm { font-family: var(--font-headline-sm); }
.font-display-lg { font-family: var(--font-display-lg); }
.font-body-lg { font-family: var(--font-body-lg); }
.font-label-caps { font-family: var(--font-label-caps); }
.text-headline-md { font-size: 32px; line-height: 1.3; font-weight: 600; }
.text-body-md { font-size: 16px; line-height: 1.6; font-weight: 400; }
.text-display-lg-mobile { font-size: 40px; line-height: 1.2; font-weight: 700; }
.text-headline-sm { font-size: 24px; line-height: 1.4; font-weight: 600; }
.text-display-lg { font-size: 64px; line-height: 1.1; letter-spacing: -0.02em; font-weight: 700; }
.text-body-lg { font-size: 18px; line-height: 1.6; font-weight: 400; }
.text-label-caps { font-size: 12px; line-height: 1.0; letter-spacing: 0.15em; font-weight: 600; }
.chiaroscuro-gradient {
background: linear-gradient(180deg, rgba(20,19,19,0) 0%, rgba(20,19,19,0.9) 80%, rgba(20,19,19,1) 100%);
}
.copper-glow {
box-shadow: inset 0 0 15px rgba(177, 109, 46, 0.1);
}
.copper-glow:hover {
box-shadow: 0 0 20px rgba(177, 109, 46, 0.3);
}
.copper-glow:focus-within {
box-shadow: 0 0 15px rgba(177, 109, 46, 0.3);
border-color: #b16d2e;
}
.vignette-overlay {
background: radial-gradient(circle, rgba(0,0,0,0) 0%, rgba(0,0,0,0.7) 100%);
}
.vignette-overlay-linear {
background: linear-gradient(to bottom, transparent 0%, rgba(20, 19, 19, 0.8) 70%, rgba(20, 19, 19, 1) 100%);
}
.section-divider {
height: 1px;
background: linear-gradient(90deg, transparent 0%, #ffb77b 50%, transparent 100%);
opacity: 0.3;
}
.booking-card {
backdrop-filter: blur(12px);
border: 0.5px solid rgba(177, 109, 46, 0.2);
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
}
}
/* Scrollbar Styles */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0e0e0e; }
::-webkit-scrollbar-thumb { background: #444748; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #ffb77b; }
/* Date/Time pickers */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
filter: invert(0.8);
cursor: pointer;
}
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+55
View File
@@ -0,0 +1,55 @@
import { Link } from '@/i18n/routing'
import { useTranslations } from 'next-intl'
export function Footer() {
const contact = useTranslations('contact')
const nav = useTranslations('nav')
const f = useTranslations('footer')
return (
<footer className="bg-surface-container-lowest w-full border-t border-outline-variant/20">
<div className="grid grid-cols-1 md:grid-cols-4 gap-[var(--spacing-gutter)] px-[var(--spacing-gutter)] py-[var(--spacing-section-padding)] max-w-[var(--spacing-container-max)] mx-auto">
<div className="col-span-1 md:col-span-1">
<span className="font-headline-sm text-headline-sm text-primary block mb-6">Teras Steakhouse</span>
<p className="font-body-md text-body-md text-on-surface-variant">
Inspired by Open Fire.<br/>
</p>
</div>
<div>
<span className="font-label-caps text-label-caps text-tertiary mb-6 block">Navigation</span>
<ul className="space-y-4">
<li><Link className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href="/menu">{nav('menu')}</Link></li>
<li><Link className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href="/rezervasyon">{nav('reservation')}</Link></li>
<li><Link className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href="/galeri">{nav('gallery')}</Link></li>
<li><Link className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href="/iletisim">{nav('contact')}</Link></li>
</ul>
</div>
<div>
<span className="font-label-caps text-label-caps text-tertiary mb-6 block">Contact</span>
<ul className="space-y-4">
<li><a className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href="https://www.instagram.com/terassteakhouse/" target="_blank" rel="noopener noreferrer">Instagram</a></li>
<li><a className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href="https://www.facebook.com/terassteakhoue/" target="_blank" rel="noopener noreferrer">Facebook</a></li>
<li><a className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href={`tel:${contact('phone').replace(/\s/g, '')}`}>{contact('phone')}</a></li>
<li><a className="font-body-md text-on-surface-variant hover:text-tertiary transition-colors" href={`mailto:${contact('email')}`}>{contact('email')}</a></li>
</ul>
</div>
<div>
<span className="font-label-caps text-label-caps text-tertiary mb-6 block">Hours</span>
<p className="text-on-surface-variant font-body-md text-body-md">
17:00 - 00:00<br/>
</p>
</div>
</div>
<div className="max-w-[var(--spacing-container-max)] mx-auto px-[var(--spacing-gutter)] py-8 border-t border-outline-variant/10 flex flex-col md:flex-row justify-between items-center">
<p className="font-body-md text-body-md text-on-surface-variant/50">© 2024 Teras Steakhouse. {f('rights')}</p>
<a href="https://ayris.tech" target="_blank" rel="noopener noreferrer" className="font-label-caps text-[10px] text-tertiary/60 hover:text-tertiary transition-colors tracking-[0.2em] uppercase mt-4 md:mt-0">
{f('created_by')}
</a>
</div>
</footer>
)
}
+75
View File
@@ -0,0 +1,75 @@
'use client'
import { Link, usePathname, useRouter } from '@/i18n/routing'
import { useTranslations, useLocale } from 'next-intl'
import { useState } from 'react'
export function Header() {
const t = useTranslations('nav')
const locale = useLocale()
const pathname = usePathname()
const router = useRouter()
const [isOpen, setIsOpen] = useState(false)
const toggleLanguage = () => {
const nextLang = locale === 'tr' ? 'en' : 'tr'
router.replace(pathname, { locale: nextLang })
}
return (
<header className="bg-surface/90 backdrop-blur-md w-full top-0 sticky z-50 border-b border-outline-variant/30 shadow-sm">
<nav className="flex justify-between items-center w-full px-[var(--spacing-gutter)] py-4 max-w-[var(--spacing-container-max)] mx-auto">
<Link href="/" className="font-headline-md text-headline-md text-on-surface tracking-tighter hover:text-tertiary transition-colors">
Teras
</Link>
<div className="hidden md:flex items-center gap-8">
<Link href="/" className={`font-label-caps text-label-caps uppercase ${pathname === '/' ? 'text-secondary font-bold border-b-2 border-secondary pb-1' : 'text-on-surface-variant hover:text-secondary transition-colors duration-300'}`}>
{t('home')}
</Link>
<Link href="/menu" className={`font-label-caps text-label-caps uppercase ${pathname === '/menu' ? 'text-secondary font-bold border-b-2 border-secondary pb-1' : 'text-on-surface-variant hover:text-secondary transition-colors duration-300'}`}>
{t('menu')}
</Link>
<Link href="/galeri" className={`font-label-caps text-label-caps uppercase ${pathname === '/galeri' ? 'text-secondary font-bold border-b-2 border-secondary pb-1' : 'text-on-surface-variant hover:text-secondary transition-colors duration-300'}`}>
{t('gallery')}
</Link>
<button onClick={toggleLanguage} className="font-label-caps text-label-caps text-on-surface-variant hover:text-tertiary transition-colors uppercase">
{locale}
</button>
</div>
<div className="flex items-center gap-4">
<Link href="/rezervasyon" className="bg-secondary-container text-on-secondary-container px-6 py-2 font-label-caps text-label-caps uppercase tracking-widest hover:scale-95 transition-all duration-200 hidden md:inline-block">
{t('reservation')}
</Link>
{/* Mobile menu button */}
<div className="md:hidden flex items-center gap-4">
<button onClick={toggleLanguage} className="font-label-caps text-label-caps text-on-surface-variant uppercase">
{locale}
</button>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-on-surface-variant hover:text-secondary"
>
<span className="material-symbols-outlined">{isOpen ? 'close' : 'menu'}</span>
</button>
</div>
</div>
</nav>
{/* Mobile Menu */}
{isOpen && (
<div className="md:hidden bg-surface border-b border-outline-variant/30">
<div className="px-4 pt-2 pb-4 space-y-2 flex flex-col">
<Link href="/" onClick={() => setIsOpen(false)} className="font-label-caps text-label-caps text-on-surface hover:text-secondary py-2 uppercase">{t('home')}</Link>
<Link href="/menu" onClick={() => setIsOpen(false)} className="font-label-caps text-label-caps text-on-surface hover:text-secondary py-2 uppercase">{t('menu')}</Link>
<Link href="/galeri" onClick={() => setIsOpen(false)} className="font-label-caps text-label-caps text-on-surface hover:text-secondary py-2 uppercase">{t('gallery')}</Link>
<Link href="/rezervasyon" onClick={() => setIsOpen(false)} className="bg-secondary-container text-on-secondary-container px-6 py-2 font-label-caps text-label-caps uppercase tracking-widest text-center mt-2">{t('reservation')}</Link>
</div>
</div>
)}
</header>
)
}
+58
View File
@@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+15
View File
@@ -0,0 +1,15 @@
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default
};
});
+10
View File
@@ -0,0 +1,10 @@
import {defineRouting} from 'next-intl/routing';
import {createNavigation} from 'next-intl/navigation';
export const routing = defineRouting({
locales: ['en', 'tr'],
defaultLocale: 'tr'
});
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
+47
View File
@@ -0,0 +1,47 @@
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
// Boilerplate mock logic
// TODO: In production, lookup user in Prisma and verify password using bcrypt
// const user = await db.user.findUnique({ where: { email: credentials.email } })
if (credentials?.email === "admin@ayris.tech" && credentials?.password === "admin") {
return {
id: "1",
name: "Admin User",
email: "admin@ayris.tech",
role: "ADMIN"
}
}
return null
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as any).role
}
return token
},
async session({ session, token }) {
if (session.user && token.role) {
(session.user as any).role = token.role
}
return session
}
},
pages: {
signIn: '/login'
}
})
+20
View File
@@ -0,0 +1,20 @@
import { v2 as cloudinary } from 'cloudinary'
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
})
export async function uploadImage(file: string, folder: string) {
const result = await cloudinary.uploader.upload(file, {
folder, transformation: [{ quality: 'auto', fetch_format: 'auto' }],
})
return { url: result.secure_url, publicId: result.public_id }
}
export async function deleteImage(publicId: string) {
await cloudinary.uploader.destroy(publicId)
}
export { cloudinary }
+9
View File
@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const db = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+133
View File
@@ -0,0 +1,133 @@
{
"nav": {
"home": "Home",
"menu": "Menu",
"gallery": "Gallery",
"reservation": "Reservation",
"contact": "Contact"
},
"home": {
"hero": {
"subtitle": "Est. 2015 • Teras Steakhouse",
"title": "Inspired by Open Fire",
"desc": "Since 2015, as Teras Steakhouse, we share our passion for high-quality meat with our guests. Our carefully prepared meats using the dry-aged method reach perfection with our meticulous cooking techniques.",
"explore_menu": "Explore the Menu",
"our_philosophy": "Our Philosophy"
},
"philosophy": {
"tag": "01 / ABOUT US",
"title": "Our Method",
"desc1": "We cook our meat on the grill, extracting every natural nuance and resting it sufficiently. No flashy herbs, just pure flavor and a little salt.",
"desc2": "This is what you get: A mouth-watering, juicy, and delicious cut of meat.",
"badge": "ARGENTINE GRILL"
},
"cuts": {
"tag": "02 / THE SELECTION",
"title": "Dry Aged & Argentine Grill",
"dry_aged": "Dry Aged Meat",
"dry_aged_desc": "A type of meat processed with a special maturation method. It allows the meat to be broken down by natural enzymes, achieving a more intense flavor.",
"grill": "Argentine Grill",
"grill_desc": "It cooks with its own juice and fat, making it juicier and more aromatic. Prepared using high-quality charcoal.",
"t_bone": "T-Bone Steak",
"t_bone_desc": "Exquisite taste with dry aged process.",
"tomahawk": "Tomahawk",
"tomahawk_desc": "Bone-in feast, Argentine grill style.",
"new_york": "New York Steak",
"new_york_desc": "Intense aroma and flawless marbling."
},
"cellar": {
"tag": "03 / CURATION",
"title": "The Cellar",
"desc": "You can find the best wines to accompany your meat in our restaurant. Whatever the cut, cooking degree, and sauce of your meat, we definitely have a wine recommendation for you.",
"wine1_name": "Cabernet Sauvignon",
"wine1_desc": "Classic Red, Full Body",
"wine2_name": "Pinot Noir",
"wine2_desc": "Light Red, Smooth Taste",
"wine3_name": "Chardonnay",
"wine3_desc": "Fresh White Selection",
"view_list": "VIEW FULL WINE LIST"
},
"cta": {
"title": "Secure Your Table",
"desc": "We look forward to providing you with an unforgettable dining experience. Please book in advance.",
"button": "RESERVE A TABLE"
}
},
"menu": {
"title": "The Menu",
"subtitle": "Forged in Ember & Ash",
"nav_starters": "Starters",
"nav_prime_cuts": "Prime Cuts",
"nav_sides": "Sides",
"nav_dessert": "Dessert",
"starters_title": "Starters",
"starters_desc": "Primal ignitions to awaken the palate. Each dish is a testament to the transformative power of smoke.",
"starter1_title": "Coal-Roasted Beetroot",
"starter1_desc": "Whipped goat cheese, toasted hazelnut, aged balsamic, honeycomb.",
"starter2_title": "Embered Bone Marrow",
"starter2_desc": "Bourbon-bacon jam, sourdough toast points, parsley salad, grey sea salt.",
"prime_cuts_tag": "The Masterpiece",
"prime_cuts_title": "Dry-Aged Tomahawk",
"prime_cuts_desc": "45-day dry-aged wagyu, carved tableside. Finished over white oak and cherry wood embers.",
"mkt_price": "MKT PRICE",
"cut1_title": "Fillet Mignon",
"cut1_desc": "Exceptionally tender with a delicate char.",
"cut2_title": "A5 Wagyu Strip",
"cut2_desc": "Miyazaki Prefecture. Intense marbling, melt-in-your-mouth texture.",
"cut3_title": "Dry-Aged Ribeye",
"cut3_desc": "Aged 30 days for concentrated mineral depth.",
"sides_title": "Accompaniments",
"sides_desc": "Refined staples crafted to complement the intensity of our proteins.",
"side1_title": "Truffle Mac & Cheese",
"side1_desc": "Black winter truffle, four-cheese blend.",
"side2_title": "Asparagus Al Forno",
"side2_desc": "Wood-fired, lemon zest, cured egg yolk.",
"dessert_title": "Sweet Release",
"dessert1_title": "Smoked Chocolate Fondant",
"dessert1_desc": "Bourbon-soaked cherries, vanilla bean gelato, salt flakes.",
"dessert2_title": "Burnt Basque Cheesecake",
"dessert2_desc": "Charred exterior, creamy center, seasonal berry compote."
},
"reservation": {
"title": "Secure Your Table",
"subtitle": "Reservation",
"date": "Date",
"time": "Time",
"guests": "Guests",
"person": "Person",
"occasion": "Occasion",
"occasion_none": "None",
"occasion_birthday": "Birthday",
"occasion_anniversary": "Anniversary",
"occasion_business": "Business",
"requests": "Special Requests",
"requests_placeholder": "Dietary requirements or preferences...",
"submit": "Confirm Availability",
"success_title": "Reservation Confirmed",
"success_desc": "An email with your booking details has been sent. We look forward to welcoming you to Teras Steakhouse.",
"book_another": "Book Another Table",
"info_title1": "The Open Fire",
"info_desc1": "Witness the primal art of live-fire cooking from our Chef's counter seats.",
"info_title2": "Curation",
"info_desc2": "Sourcing only the finest cuts of heritage beef, dry-aged in-house for 45 days.",
"info_title3": "Cellar",
"info_desc3": "A sommelier-curated selection of rare vintages and bold reds."
},
"contact": {
"title": "Contact",
"address": "Ölüdeniz, Atatürk Cd. No:36, 48340 Fethiye/Muğla, Turkey",
"phone": "+90 537 420 52 80",
"email": "dincergokce48@gmail.com",
"form_name": "Your Name",
"form_email": "Email",
"form_message": "Your Message",
"form_submit": "Send"
},
"footer": {
"rights": "All rights reserved.",
"created_by": "Created by ayris.tech",
"address": "Ölüdeniz, Atatürk Cd. No:36, Fethiye/Muğla",
"phone": "+90 537 420 52 80",
"email": "dincergokce48@gmail.com"
}
}
+133
View File
@@ -0,0 +1,133 @@
{
"nav": {
"home": "Ana Sayfa",
"menu": "Menü",
"gallery": "Galeri",
"reservation": "Rezervasyon",
"contact": "İletişim"
},
"home": {
"hero": {
"subtitle": "Est. 2015 • Teras Steakhouse",
"title": "Inspired by Open Fire",
"desc": "Teras Steakhouse olarak, kaliteli et konusundaki tutkumuzu misafirlerimize sunuyoruz. Dry aged yöntemiyle özenle hazırladığımız etlerimiz, titiz pişirme tekniklerimizle mükemmelliğe ulaşıyor.",
"explore_menu": "Menüyü İncele",
"our_philosophy": "Yöntemimiz"
},
"philosophy": {
"tag": "01 / HAKKIMIZDA",
"title": "Yöntemimiz",
"desc1": "Etimizi ızgarada pişiriyoruz, her doğal nüansı çıkararak ve yeterince dinlendirerek. İhtişamlı otlar yok, sadece saf lezzet ve biraz tuzla.",
"desc2": "Gördüğünüz şey budur: Ağzı sulandıran, sulu ve lezzetli bir et dilimi.",
"badge": "ARJANTİN IZGARA"
},
"cuts": {
"tag": "02 / SEÇKİ",
"title": "Dry Aged & Arjantin Izgara",
"dry_aged": "Dry Aged Et",
"dry_aged_desc": "Özel bir olgunlaştırma yöntemiyle işlenen et çeşididir. Etin doğal enzimlerle parçalanmasını sağlar ve daha yoğun bir lezzet elde edilir.",
"grill": "Arjantin Izgara",
"grill_desc": "Kendi suyu ve yağı ile pişmesi sayesinde daha sulu ve aromatiktir. Yüksek kaliteli kömür kullanılarak hazırlanır.",
"t_bone": "T-Bone Steak",
"t_bone_desc": "Dry aged işlemi görmüş enfes lezzet.",
"tomahawk": "Tomahawk",
"tomahawk_desc": "Kemikli şölen, Arjantin ızgara usulü.",
"new_york": "New York Steak",
"new_york_desc": "Yoğun aroma ve kusursuz mermerlenme."
},
"cellar": {
"tag": "03 / KAV",
"title": "Şaraplar",
"desc": "Restoranımızda etinize eşlik edecek en iyi şarapları bulabilirsiniz. Klasik kırmızı şaraplarımızdan Cabernet Sauvignon, Malbec ile etinizin lezzetini arttırabilirsiniz. Beyaz şarap tercih edenler için de Chardonnay, Sauvignon Blanc gibi seçeneklerimiz mevcuttur.",
"wine1_name": "Cabernet Sauvignon",
"wine1_desc": "Klasik Kırmızı, Güçlü Gövde",
"wine2_name": "Pinot Noir",
"wine2_desc": "Hafif Kırmızı, Yumuşak İçim",
"wine3_name": "Chardonnay",
"wine3_desc": "Ferah Beyaz Seçimi",
"view_list": "ŞARAP MENÜSÜNÜ GÖR"
},
"cta": {
"title": "Masanızı Ayırtın",
"desc": "Size unutulmaz bir yemek deneyimi sunmak için sabırsızlanıyoruz. Lütfen önceden rezervasyon yaptırınız.",
"button": "REZERVASYON YAP"
}
},
"menu": {
"title": "Menü",
"subtitle": "Ateş ve Külün Eseri",
"nav_starters": "Başlangıçlar",
"nav_prime_cuts": "Ana Yemekler",
"nav_sides": "Yan Lezzetler",
"nav_dessert": "Tatlılar",
"starters_title": "Başlangıçlar",
"starters_desc": "Ateşin dönüştürücü gücünü hissedeceğiniz ilk lezzetler.",
"starter1_title": "Közlenmiş Pancar",
"starter1_desc": "Keçi peyniri, fındık, balzamik, petek bal.",
"starter2_title": "Közde İlik",
"starter2_desc": "Kızarmış ekşi mayalı ekmek, deniz tuzu, maydanoz salatası.",
"prime_cuts_tag": "Başyapıt",
"prime_cuts_title": "Dry-Aged Tomahawk",
"prime_cuts_desc": "45 gün dinlendirilmiş, masanızda dilimlenen, meşe odunu közünde mühürlenmiş Tomahawk.",
"mkt_price": "GÜNLÜK FİYAT",
"cut1_title": "Fillet Mignon",
"cut1_desc": "Yumuşacık dokusu ve hafif köz aroması.",
"cut2_title": "A5 Wagyu Strip",
"cut2_desc": "Ağızda dağılan kusursuz mermerlenme.",
"cut3_title": "Dry-Aged Ribeye",
"cut3_desc": "30 gün dinlendirilmiş, yoğun aromalı antrikot.",
"sides_title": "Yan Lezzetler",
"sides_desc": "Etinizin yoğunluğunu tamamlamak için özenle seçilmiş eşlikçiler.",
"side1_title": "Trüflü Mac & Cheese",
"side1_desc": "Siyah kış trüfü, dört peynir karışımı.",
"side2_title": "Fırın Kuşkonmaz",
"side2_desc": "Odun ateşinde pişmiş, limon rendesi.",
"dessert_title": "Tatlı Son",
"dessert1_title": "İsli Çikolatalı Fondan",
"dessert1_desc": "Vanilyalı dondurma ve deniz tuzu taneleri.",
"dessert2_title": "Yanık Bask Cheesecake",
"dessert2_desc": "Mevsim meyveleri kompostosu ile."
},
"reservation": {
"title": "Güvenli Rezervasyon",
"subtitle": "Rezervasyon",
"date": "Tarih",
"time": "Saat",
"guests": "Kişi Sayısı",
"person": "Kişi",
"occasion": "Özel Gün",
"occasion_none": "Yok",
"occasion_birthday": "Doğum Günü",
"occasion_anniversary": "Yıl Dönümü",
"occasion_business": "İş Yemeği",
"requests": "Özel İstekler",
"requests_placeholder": "Alerji veya özel notlarınız...",
"submit": "Uygunluk Kontrol Et",
"success_title": "Rezervasyon Onaylandı",
"success_desc": "Rezervasyon detaylarınızı içeren bir e-posta gönderildi. Sizi Teras Steakhouse'da ağırlamak için sabırsızlanıyoruz.",
"book_another": "Yeni Rezervasyon Yap",
"info_title1": "Açık Ateş",
"info_desc1": "Canlı ateşle pişirme sanatına şahit olun.",
"info_title2": "Kürasyon",
"info_desc2": "45 gün boyunca kendi bünyemizde kuru dinlendirilmiş (dry-aged) en iyi miras dana etleri.",
"info_title3": "Kav",
"info_desc3": "Nadir rekoltelerden ve cesur kırmızılardan oluşan özel şarap seçkisi."
},
"contact": {
"title": "İletişim",
"address": "Ölüdeniz, Atatürk Cd. No:36, 48340 Fethiye/Muğla, Turkey",
"phone": "+90 537 420 52 80",
"email": "dincergokce48@gmail.com",
"form_name": "Adınız",
"form_email": "E-posta",
"form_message": "Mesajınız",
"form_submit": "Gönder"
},
"footer": {
"rights": "Tüm hakları saklıdır.",
"created_by": "Created by ayris.tech",
"address": "Ölüdeniz, Atatürk Cd. No:36, Fethiye/Muğla",
"phone": "+90 537 420 52 80",
"email": "dincergokce48@gmail.com"
}
}
+16
View File
@@ -0,0 +1,16 @@
import type { NextConfig } from 'next'
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'res.cloudinary.com' },
{ protocol: 'https', hostname: 'images.unsplash.com' },
],
},
}
export default withNextIntl(nextConfig)
+9296
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "terassteak",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@prisma/client": "^5.14.0",
"class-variance-authority": "^0.7.1",
"cloudinary": "^2.10.0",
"clsx": "^2.1.1",
"developer-icons": "^7.0.1",
"framer-motion": "^12.40.0",
"lucide-react": "^1.18.0",
"next": "16.2.9",
"next-auth": "^5.0.0-beta.31",
"next-intl": "^4.13.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"shadcn": "^4.11.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"prisma": "^5.14.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+61
View File
@@ -0,0 +1,61 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
USER
}
model User {
id String @id @default(cuid())
name String?
email String @unique
password String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
accounts Account[]
sessions Session[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
+20
View File
@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server'
import createMiddleware from 'next-intl/middleware'
import { auth } from '@/lib/auth'
import { routing } from '@/i18n/routing'
const intlMiddleware = createMiddleware(routing)
export async function proxy(request: NextRequest) {
if (request.nextUrl.pathname.includes('/admin')) {
const session = await auth()
if (!session || (session.user as any)?.role !== 'ADMIN') {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return intlMiddleware(request)
}
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
}
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}