first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
.next
|
||||
node_modules
|
||||
.env
|
||||
.env.*.local
|
||||
.git
|
||||
.vscode
|
||||
docs
|
||||
README.md
|
||||
AGENTS.md
|
||||
+41
@@ -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
|
||||
@@ -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 -->
|
||||
+34
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/lib/auth"
|
||||
|
||||
export const { GET, POST } = handlers
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+154
@@ -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;
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
})
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
Generated
+9296
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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|.*\\..*).*)']
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user