kite-qr
@@ -0,0 +1,43 @@
|
|||||||
|
# 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/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
/app/generated/prisma
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 1. Base image
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# 2. Dependencies
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
|
||||||
|
# 3. Builder
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Environment variables must be present at build time for Next.js
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Generate Prisma client before building
|
||||||
|
COPY prisma ./prisma
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 4. Runner
|
||||||
|
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
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
|
||||||
|
import { CategoryNav } from "@/components/CategoryNav";
|
||||||
|
import { MenuItem } from "@/components/MenuItem";
|
||||||
|
|
||||||
|
export const CATEGORY_ICONS: Record<string, string> = {
|
||||||
|
"kahvaltiliklar": "🌅",
|
||||||
|
"kaseler": "🥗",
|
||||||
|
"burger-ve-sandvic": "🍔",
|
||||||
|
"pizzalar": "🍕",
|
||||||
|
"makarnalar": "🍝",
|
||||||
|
"baslangiclar": "🧆",
|
||||||
|
"salatalar": "🥬",
|
||||||
|
"ana-yemekler": "🍽️",
|
||||||
|
"tatlilar": "🍮",
|
||||||
|
"i̇mza-kokteyller": "🍹",
|
||||||
|
"klasik-kokteyller": "🍸",
|
||||||
|
"biralar": "🍺",
|
||||||
|
"shotlar": "🥃",
|
||||||
|
"sise-ve-kadeh-alkollu": "🥂",
|
||||||
|
"saraplar": "🍷",
|
||||||
|
"cerezler": "🥜",
|
||||||
|
"alkolsuz-i̇cecekler": "🧃",
|
||||||
|
"sicak-kahveler": "☕",
|
||||||
|
"soguk-kahveler": "🧊",
|
||||||
|
"ekstralar": "✨",
|
||||||
|
};
|
||||||
|
|
||||||
|
type SiteSettings = {
|
||||||
|
logoUrl: string
|
||||||
|
location: string
|
||||||
|
restaurantName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MenuClient({ initialCategories, siteSettings }: { initialCategories: MenuCategory[], siteSettings: SiteSettings }) {
|
||||||
|
const [activeCategoryId, setActiveCategoryId] = useState<string>(
|
||||||
|
initialCategories[0]?.id || ""
|
||||||
|
);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<MenuItemType | null>(null);
|
||||||
|
const sectionRefs = useRef<(HTMLElement | null)[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const hit = entries.find((e) => e.isIntersecting);
|
||||||
|
if (hit) setActiveCategoryId(hit.target.id);
|
||||||
|
},
|
||||||
|
{ root: null, rootMargin: "-100px 0px -62% 0px", threshold: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
sectionRefs.current.forEach((ref) => ref && observer.observe(ref));
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedItem) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [selectedItem]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ background: "var(--cream)" }}>
|
||||||
|
|
||||||
|
{/* ── Hero ─────────────────────────────────────── */}
|
||||||
|
<header className="relative overflow-hidden bg-stone-900">
|
||||||
|
{/* Background Image */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0 opacity-80"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/header-bg.jpg')",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Dark overlay to make text readable */}
|
||||||
|
<div className="absolute inset-0 z-0 bg-gradient-to-b from-black/60 via-black/40 to-black/80" />
|
||||||
|
|
||||||
|
{/* hero content */}
|
||||||
|
<div className="relative z-10 flex flex-col items-center text-center px-6 pt-16 pb-20">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="text-[10px] tracking-[0.38em] uppercase mb-8 font-sans font-medium"
|
||||||
|
style={{ color: "rgba(255,220,160,1)" }}
|
||||||
|
>
|
||||||
|
Akyaka · Muğla
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Logo */}
|
||||||
|
<img
|
||||||
|
src="/logo.png"
|
||||||
|
alt="Moy Beach"
|
||||||
|
className="w-64 md:w-80 object-contain drop-shadow-lg"
|
||||||
|
style={{ filter: 'brightness(0) invert(1)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 my-5"
|
||||||
|
style={{ color: "rgba(255,190,100,0.35)" }}
|
||||||
|
>
|
||||||
|
<div className="h-px w-14" style={{ background: "currentColor" }} />
|
||||||
|
<span className="text-base">✦</span>
|
||||||
|
<div className="h-px w-14" style={{ background: "currentColor" }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="text-xs font-sans tracking-wide"
|
||||||
|
style={{ color: "rgba(255,218,168,0.5)" }}
|
||||||
|
>
|
||||||
|
Akyaka'nın en keyifli menüsü
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* wave transition */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 translate-y-px">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1440 52"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
className="w-full h-[52px]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 52L80 44C160 36 320 20 480 18C640 16 800 28 960 32C1120 36 1280 32 1360 30L1440 28V52H1360C1280 52 1120 52 960 52C800 52 640 52 480 52C320 52 160 52 80 52H0Z"
|
||||||
|
fill="#FAF8F3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* ── Category Nav ─────────────────────────────── */}
|
||||||
|
<CategoryNav
|
||||||
|
categories={initialCategories}
|
||||||
|
activeCategoryId={activeCategoryId}
|
||||||
|
icons={CATEGORY_ICONS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ── Menu Sections ────────────────────────────── */}
|
||||||
|
<main className="pb-28">
|
||||||
|
{initialCategories.map((category, index) => (
|
||||||
|
<motion.section
|
||||||
|
key={category.id}
|
||||||
|
id={category.id}
|
||||||
|
ref={(el) => { sectionRefs.current[index] = el; }}
|
||||||
|
className="pt-10 px-4 max-w-lg mx-auto scroll-mt-[62px]"
|
||||||
|
initial={{ opacity: 0, y: 14 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
|
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
{/* section header */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2.5 mb-4 pb-3 border-b"
|
||||||
|
style={{ borderColor: "var(--sand-border)" }}
|
||||||
|
>
|
||||||
|
<span className="text-xl leading-none select-none">
|
||||||
|
{CATEGORY_ICONS[category.id] ?? "🍽️"}
|
||||||
|
</span>
|
||||||
|
<h2
|
||||||
|
className="font-display text-[22px] leading-tight font-semibold italic"
|
||||||
|
style={{ color: "var(--stone-ink)" }}
|
||||||
|
>
|
||||||
|
{category.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{category.items.map((item, idx) => (
|
||||||
|
<MenuItem key={idx} item={item} onClick={() => setSelectedItem(item)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
))}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* ── Footer ───────────────────────────────────── */}
|
||||||
|
<footer
|
||||||
|
className="py-12 text-center"
|
||||||
|
style={{ background: "var(--hero-from)" }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
{/* Footer Logo */}
|
||||||
|
<img
|
||||||
|
src="/logo.png"
|
||||||
|
alt="Moy Beach"
|
||||||
|
className="w-36 object-contain opacity-70 mb-2"
|
||||||
|
style={{ filter: 'brightness(0) invert(1)' }}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className="text-[10px] font-sans tracking-[0.3em] uppercase mt-1"
|
||||||
|
style={{ color: "rgba(255,200,130,0.35)" }}
|
||||||
|
>
|
||||||
|
Akyaka, Muğla
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 my-4"
|
||||||
|
style={{ color: "rgba(160,100,50,0.4)" }}
|
||||||
|
>
|
||||||
|
<div className="h-px w-10" style={{ background: "currentColor" }} />
|
||||||
|
<span className="text-xs">✦</span>
|
||||||
|
<div className="h-px w-10" style={{ background: "currentColor" }} />
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-[11px] font-sans"
|
||||||
|
style={{ color: "rgba(120,90,60,0.5)" }}
|
||||||
|
>
|
||||||
|
© 2026 Moy Beach. Tüm hakları saklıdır.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{/* ── Modal ────────────────────────────────────── */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedItem && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
|
||||||
|
onClick={() => setSelectedItem(null)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 50, opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ y: 0, opacity: 1, scale: 1 }}
|
||||||
|
exit={{ y: 20, opacity: 0, scale: 0.95 }}
|
||||||
|
className="bg-white rounded-2xl overflow-hidden w-full max-w-sm shadow-xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{selectedItem.image && (
|
||||||
|
<div className="w-full h-56 relative">
|
||||||
|
<img src={selectedItem.image} alt={selectedItem.name} className="w-full h-full object-cover" />
|
||||||
|
<button
|
||||||
|
className="absolute top-3 right-3 bg-black/50 text-white w-8 h-8 rounded-full flex items-center justify-center backdrop-blur-md transition-colors hover:bg-black/70"
|
||||||
|
onClick={() => setSelectedItem(null)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="flex justify-between items-start gap-4 mb-2">
|
||||||
|
<h3 className="text-xl font-semibold font-sans" style={{ color: "var(--stone-ink)" }}>
|
||||||
|
{selectedItem.name}
|
||||||
|
</h3>
|
||||||
|
<span className="shrink-0 text-lg font-semibold font-sans tabular-nums" style={{ color: "var(--amber)" }}>
|
||||||
|
{selectedItem.price}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[14px] leading-relaxed font-sans" style={{ color: "var(--stone-muted)" }}>
|
||||||
|
{selectedItem.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { createCategory, updateCategory } from './actions'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
type Category = {
|
||||||
|
id: number
|
||||||
|
name_tr: string
|
||||||
|
order_num: number | null
|
||||||
|
status: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CategoryForm({ category }: { category?: Category }) {
|
||||||
|
const isEditing = !!category
|
||||||
|
|
||||||
|
// Use appropriate action based on editing state
|
||||||
|
const actionToUse = isEditing
|
||||||
|
? updateCategory.bind(null, category.id)
|
||||||
|
: createCategory
|
||||||
|
|
||||||
|
const [state, formAction, isPending] = useActionState(actionToUse, undefined)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={formAction} className="space-y-6">
|
||||||
|
{state?.error && (
|
||||||
|
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Kategori Adı (TR) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name_tr"
|
||||||
|
defaultValue={category?.name_tr || ''}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Sıra Numarası
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="order_num"
|
||||||
|
defaultValue={category?.order_num || 0}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="status"
|
||||||
|
id="status"
|
||||||
|
defaultChecked={category ? category.status === 1 : true}
|
||||||
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="status" className="text-sm font-medium text-gray-700">
|
||||||
|
Aktif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Link href="/admin/categories" className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors">
|
||||||
|
İptal
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{isPending ? 'Kaydediliyor...' : 'Kaydet'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
|
import { deleteCategory } from './actions'
|
||||||
|
|
||||||
|
export default function DeleteButton({ id }: { id: number }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (confirm('Bu kategoriyi silmek istediğinize emin misiniz?')) {
|
||||||
|
const res = await deleteCategory(id)
|
||||||
|
if (res?.error) {
|
||||||
|
alert(res.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="Sil"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import CategoryForm from '../../CategoryForm'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Kategori Düzenle - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function EditCategoryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const resolvedParams = await params
|
||||||
|
const id = parseInt(resolvedParams.id, 10)
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await prisma.categories.findUnique({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Kategori Düzenle</h1>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-2xl">
|
||||||
|
<CategoryForm category={category} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export async function createCategory(_prevState: unknown, formData: FormData) {
|
||||||
|
const name_tr = formData.get('name_tr') as string
|
||||||
|
const name = formData.get('name') as string || name_tr
|
||||||
|
const order_num = parseInt(formData.get('order_num') as string, 10) || 0
|
||||||
|
const status = formData.get('status') === 'on' ? 1 : 0
|
||||||
|
|
||||||
|
if (!name_tr) {
|
||||||
|
return { error: 'Lütfen kategori adını doldurun.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.categories.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
name_tr,
|
||||||
|
slug: name_tr.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||||
|
order_num,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create category error:', error)
|
||||||
|
return { error: 'Kategori eklenirken hata oluştu.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/admin/categories')
|
||||||
|
revalidatePath('/')
|
||||||
|
redirect('/admin/categories')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCategory(id: number, _prevState: unknown, formData: FormData) {
|
||||||
|
const name_tr = formData.get('name_tr') as string
|
||||||
|
const name = formData.get('name') as string || name_tr
|
||||||
|
const order_num = parseInt(formData.get('order_num') as string, 10) || 0
|
||||||
|
const status = formData.get('status') === 'on' ? 1 : 0
|
||||||
|
|
||||||
|
if (!name_tr) {
|
||||||
|
return { error: 'Lütfen kategori adını doldurun.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.categories.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
name_tr,
|
||||||
|
slug: name_tr.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||||
|
order_num,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update category error:', error)
|
||||||
|
return { error: 'Kategori güncellenirken hata oluştu.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/admin/categories')
|
||||||
|
revalidatePath('/')
|
||||||
|
redirect('/admin/categories')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCategory(id: number) {
|
||||||
|
try {
|
||||||
|
// Check if category has products
|
||||||
|
const productsCount = await prisma.products.count({
|
||||||
|
where: { category_id: id }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (productsCount > 0) {
|
||||||
|
return { error: 'Bu kategoriye ait ürünler olduğu için silinemez. Önce ürünleri silin veya başka kategoriye taşıyın.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.categories.delete({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
|
||||||
|
revalidatePath('/admin/categories')
|
||||||
|
revalidatePath('/')
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete category error:', error)
|
||||||
|
return { error: 'Silme işlemi başarısız oldu.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import CategoryForm from '../CategoryForm'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Yeni Kategori Ekle - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewCategoryPage() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Yeni Kategori Ekle</h1>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-2xl">
|
||||||
|
<CategoryForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { Pencil, Plus } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import DeleteButton from './DeleteButton'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Kategoriler - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CategoriesPage() {
|
||||||
|
const categories = await prisma.categories.findMany({
|
||||||
|
orderBy: { order_num: 'asc' },
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: { products: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Kategoriler</h1>
|
||||||
|
<Link href="/admin/categories/new" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-colors">
|
||||||
|
<Plus size={18} /> Yeni Kategori Ekle
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 border-b border-gray-100 text-sm font-semibold text-gray-600">
|
||||||
|
<th className="py-4 px-6">Kategori Adı</th>
|
||||||
|
<th className="py-4 px-6 text-center">Sıra</th>
|
||||||
|
<th className="py-4 px-6 text-center">Ürün Sayısı</th>
|
||||||
|
<th className="py-4 px-6 text-center">Durum</th>
|
||||||
|
<th className="py-4 px-6 text-right">İşlemler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<tr key={category.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<td className="py-4 px-6 font-medium text-gray-900">{category.name_tr}</td>
|
||||||
|
<td className="py-4 px-6 text-center text-gray-600">{category.order_num}</td>
|
||||||
|
<td className="py-4 px-6 text-center">
|
||||||
|
<span className="bg-blue-50 text-blue-700 py-1 px-3 rounded-full text-xs font-medium">
|
||||||
|
{category._count.products} Ürün
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-6 text-center">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
category.status === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{category.status === 1 ? 'Aktif' : 'Pasif'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-6 text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Link href={`/admin/categories/${category.id}/edit`} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Düzenle">
|
||||||
|
<Pencil size={18} />
|
||||||
|
</Link>
|
||||||
|
<DeleteButton id={category.id} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{categories.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="py-8 text-center text-gray-500">
|
||||||
|
Henüz kategori bulunmuyor.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { LayoutDashboard, LogOut, Package, Settings, Tags, Users2 } from 'lucide-react'
|
||||||
|
import { logout } from '@/lib/auth'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function AdminLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-gray-100 overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="w-64 bg-white shadow-md flex flex-col hidden md:flex">
|
||||||
|
<div className="p-4 border-b">
|
||||||
|
<h2 className="text-xl font-bold text-gray-800">Admin Panel</h2>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 p-4 space-y-2">
|
||||||
|
<Link href="/admin" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<LayoutDashboard size={20} />
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
<Link href="/admin/categories" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<Tags size={20} />
|
||||||
|
<span>Kategoriler</span>
|
||||||
|
</Link>
|
||||||
|
<Link href="/admin/products" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<Package size={20} />
|
||||||
|
<span>Ürünler</span>
|
||||||
|
</Link>
|
||||||
|
<Link href="/admin/settings" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<Settings size={20} />
|
||||||
|
<span>Ayarlar</span>
|
||||||
|
</Link>
|
||||||
|
<Link href="/admin/users" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<Users2 size={20} />
|
||||||
|
<span>Kullanıcılar</span>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
<div className="p-4 border-t">
|
||||||
|
<form action={async () => {
|
||||||
|
'use server'
|
||||||
|
await logout()
|
||||||
|
redirect('/admin/login')
|
||||||
|
}}>
|
||||||
|
<button className="flex items-center gap-3 w-full px-3 py-2 text-red-600 rounded-lg hover:bg-red-50 transition-colors">
|
||||||
|
<LogOut size={20} />
|
||||||
|
<span>Çıkış Yap</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 overflow-y-auto bg-gray-50 p-6 md:p-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Mobile Bottom Nav (Optional, simpler for now just a top bar) */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { Package, Tags, Eye, TrendingUp } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Admin Dashboard - Moy Beach',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AdminDashboard() {
|
||||||
|
const [totalCategories, totalProducts, activeProducts] = await Promise.all([
|
||||||
|
prisma.categories.count(),
|
||||||
|
prisma.products.count(),
|
||||||
|
prisma.products.count({ where: { status: 1 } }),
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-blue-100 text-blue-600 rounded-lg">
|
||||||
|
<Tags size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-medium">Kategori Sayısı</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{totalCategories}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-indigo-100 text-indigo-600 rounded-lg">
|
||||||
|
<Package size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-medium">Toplam Ürün</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{totalProducts}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-green-100 text-green-600 rounded-lg">
|
||||||
|
<TrendingUp size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 font-medium">Aktif Ürünler</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{activeProducts}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Hızlı Kısayollar</h2>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Link href="/admin/products" className="text-blue-600 hover:text-blue-800 font-medium flex items-center gap-2">
|
||||||
|
<Package size={18} /> Ürünleri Yönet
|
||||||
|
</Link>
|
||||||
|
<Link href="/admin/categories" className="text-blue-600 hover:text-blue-800 font-medium flex items-center gap-2">
|
||||||
|
<Tags size={18} /> Kategorileri Yönet
|
||||||
|
</Link>
|
||||||
|
<Link href="/" target="_blank" className="text-gray-600 hover:text-gray-800 font-medium flex items-center gap-2">
|
||||||
|
<Eye size={18} /> Canlı Menüyü Görüntüle
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
|
import { deleteProduct } from './actions'
|
||||||
|
|
||||||
|
export default function DeleteButton({ id }: { id: number }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (confirm('Bu ürünü silmek istediğinize emin misiniz?')) {
|
||||||
|
await deleteProduct(id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="Sil"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState, useState, useRef } from 'react'
|
||||||
|
import { createProduct, updateProduct } from './actions'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { Upload, X, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
|
const DEFAULT_IMAGE = '/default-product.png'
|
||||||
|
|
||||||
|
type Category = {
|
||||||
|
id: number
|
||||||
|
name_tr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product = {
|
||||||
|
id: number
|
||||||
|
name_tr: string
|
||||||
|
price: number | string
|
||||||
|
category_id: number | null
|
||||||
|
description_tr: string | null
|
||||||
|
image_url: string | null
|
||||||
|
status: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductForm({ categories, product }: { categories: Category[], product?: Product }) {
|
||||||
|
const isEditing = !!product
|
||||||
|
const actionToUse = isEditing
|
||||||
|
? updateProduct.bind(null, product.id)
|
||||||
|
: createProduct
|
||||||
|
|
||||||
|
const [state, formAction, isPending] = useActionState(actionToUse, undefined)
|
||||||
|
const [imageUrl, setImageUrl] = useState<string>(product?.image_url || '')
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const [uploadError, setUploadError] = useState('')
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const previewSrc = imageUrl || DEFAULT_IMAGE
|
||||||
|
|
||||||
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
setUploadError("Dosya boyutu 5MB'ı geçemez.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadError('')
|
||||||
|
setUploading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
const res = await fetch('/api/upload', { method: 'POST', body: fd })
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Yükleme başarısız')
|
||||||
|
setImageUrl(data.url)
|
||||||
|
} catch (err: any) {
|
||||||
|
setUploadError(err.message || 'Resim yüklenirken hata oluştu.')
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={formAction} className="space-y-6">
|
||||||
|
{state?.error && (
|
||||||
|
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image Upload */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Ürün Resmi</label>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Preview */}
|
||||||
|
<div className="relative w-28 h-28 rounded-xl overflow-hidden border-2 border-gray-200 bg-gray-50 flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={previewSrc}
|
||||||
|
alt="Ürün resmi önizleme"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
{imageUrl && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setImageUrl('')
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||||
|
}}
|
||||||
|
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-0.5 hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload button */}
|
||||||
|
<div className="flex flex-col gap-2 justify-center h-28">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-600 hover:border-blue-400 hover:text-blue-600 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{uploading
|
||||||
|
? <><Loader2 size={16} className="animate-spin" /> Yükleniyor...</>
|
||||||
|
: <><Upload size={16} /> Resim Yükle</>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-gray-500">PNG, JPG, WEBP — Maks. 5MB</p>
|
||||||
|
{uploadError && <p className="text-xs text-red-600">{uploadError}</p>}
|
||||||
|
{!imageUrl && (
|
||||||
|
<p className="text-xs text-gray-400 italic">Resim yüklenmezse varsayılan görsel kullanılır.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{/* URL'yi form verisiyle gönder */}
|
||||||
|
<input type="hidden" name="image_url" value={imageUrl} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ürün Adı (TR) *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name_tr"
|
||||||
|
defaultValue={product?.name_tr || ''}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kategori *</label>
|
||||||
|
<select
|
||||||
|
name="category_id"
|
||||||
|
defaultValue={product?.category_id?.toString() || ''}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Seçiniz</option>
|
||||||
|
{categories.map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.name_tr}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Fiyat (₺) *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
name="price"
|
||||||
|
defaultValue={product ? product.price.toString() : ''}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Açıklama (TR)</label>
|
||||||
|
<textarea
|
||||||
|
name="description_tr"
|
||||||
|
defaultValue={product?.description_tr || ''}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="status"
|
||||||
|
id="status"
|
||||||
|
defaultChecked={product ? product.status === 1 : true}
|
||||||
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="status" className="text-sm font-medium text-gray-700">Aktif</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Link
|
||||||
|
href="/admin/products"
|
||||||
|
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending || uploading}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{isPending ? 'Kaydediliyor...' : 'Kaydet'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import ProductForm from '../../ProductForm'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Ürün Düzenle - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const resolvedParams = await params
|
||||||
|
const id = parseInt(resolvedParams.id, 10)
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [categories, product] = await Promise.all([
|
||||||
|
prisma.categories.findMany({
|
||||||
|
orderBy: { order_num: 'asc' },
|
||||||
|
select: { id: true, name_tr: true }
|
||||||
|
}),
|
||||||
|
prisma.products.findUnique({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Ürün Düzenle</h1>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-3xl">
|
||||||
|
<ProductForm
|
||||||
|
categories={categories}
|
||||||
|
product={{
|
||||||
|
...product,
|
||||||
|
price: product.price.toString() // Convert Decimal to string
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export async function createProduct(_prevState: unknown, formData: FormData) {
|
||||||
|
const name_tr = formData.get('name_tr') as string
|
||||||
|
const name = formData.get('name') as string || name_tr
|
||||||
|
const price = parseFloat(formData.get('price') as string)
|
||||||
|
const category_id = parseInt(formData.get('category_id') as string, 10)
|
||||||
|
const description_tr = formData.get('description_tr') as string || ''
|
||||||
|
const image_url = formData.get('image_url') as string || null
|
||||||
|
const status = formData.get('status') === 'on' ? 1 : 0
|
||||||
|
|
||||||
|
if (!name_tr || isNaN(price) || isNaN(category_id)) {
|
||||||
|
return { error: 'Lütfen zorunlu alanları doldurun.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.products.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
name_tr,
|
||||||
|
slug: name_tr.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||||
|
price,
|
||||||
|
category_id,
|
||||||
|
description_tr,
|
||||||
|
image_url,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create product error:', error)
|
||||||
|
return { error: 'Ürün eklenirken hata oluştu.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/admin/products')
|
||||||
|
revalidatePath('/')
|
||||||
|
redirect('/admin/products')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProduct(id: number, _prevState: unknown, formData: FormData) {
|
||||||
|
const name_tr = formData.get('name_tr') as string
|
||||||
|
const name = formData.get('name') as string || name_tr
|
||||||
|
const price = parseFloat(formData.get('price') as string)
|
||||||
|
const category_id = parseInt(formData.get('category_id') as string, 10)
|
||||||
|
const description_tr = formData.get('description_tr') as string || ''
|
||||||
|
const image_url = formData.get('image_url') as string || null
|
||||||
|
const status = formData.get('status') === 'on' ? 1 : 0
|
||||||
|
|
||||||
|
if (!name_tr || isNaN(price) || isNaN(category_id)) {
|
||||||
|
return { error: 'Lütfen zorunlu alanları doldurun.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.products.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
name_tr,
|
||||||
|
slug: name_tr.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||||
|
price,
|
||||||
|
category_id,
|
||||||
|
description_tr,
|
||||||
|
image_url,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update product error:', error)
|
||||||
|
return { error: 'Ürün güncellenirken hata oluştu.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/admin/products')
|
||||||
|
revalidatePath('/')
|
||||||
|
redirect('/admin/products')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProduct(id: number) {
|
||||||
|
try {
|
||||||
|
await prisma.products.delete({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
revalidatePath('/admin/products')
|
||||||
|
revalidatePath('/')
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete product error:', error)
|
||||||
|
return { error: 'Silme işlemi başarısız oldu.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import ProductForm from '../ProductForm'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Yeni Ürün Ekle - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function NewProductPage() {
|
||||||
|
const categories = await prisma.categories.findMany({
|
||||||
|
orderBy: { order_num: 'asc' },
|
||||||
|
select: { id: true, name_tr: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Yeni Ürün Ekle</h1>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-3xl">
|
||||||
|
<ProductForm categories={categories} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { Pencil, Plus, Trash2, Image as ImageIcon } from 'lucide-react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import DeleteButton from './DeleteButton'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Ürün Yönetimi - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProductsPage() {
|
||||||
|
const categories = await prisma.categories.findMany({
|
||||||
|
orderBy: { order_num: 'asc' },
|
||||||
|
include: {
|
||||||
|
products: {
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Ürün Yönetimi</h1>
|
||||||
|
<Link href="/admin/products/new" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
|
||||||
|
<Plus size={20} />
|
||||||
|
<span>Yeni Ürün Ekle</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div key={category.id} className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
<div className="bg-gray-50 px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
||||||
|
<h2 className="text-lg font-bold text-gray-800">{category.name_tr}</h2>
|
||||||
|
<span className="text-sm text-gray-500 font-medium">{category.products.length} ürün</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{category.products.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-100">
|
||||||
|
<th className="py-3 px-6 text-sm font-semibold text-gray-600 w-16">Resim</th>
|
||||||
|
<th className="py-3 px-6 text-sm font-semibold text-gray-600">İsim</th>
|
||||||
|
<th className="py-3 px-6 text-sm font-semibold text-gray-600">Fiyat</th>
|
||||||
|
<th className="py-3 px-6 text-sm font-semibold text-gray-600">Durum</th>
|
||||||
|
<th className="py-3 px-6 text-sm font-semibold text-gray-600 text-right">İşlemler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-50">
|
||||||
|
{category.products.map((product) => (
|
||||||
|
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<td className="py-3 px-6">
|
||||||
|
{product.image_url ? (
|
||||||
|
<div className="relative w-12 h-12 rounded-lg overflow-hidden border border-gray-200">
|
||||||
|
<Image
|
||||||
|
src={product.image_url}
|
||||||
|
alt={product.name_tr}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center text-gray-400 border border-gray-200">
|
||||||
|
<ImageIcon size={20} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-6 font-medium text-gray-900">{product.name_tr}</td>
|
||||||
|
<td className="py-3 px-6 text-gray-600">₺{product.price.toString()}</td>
|
||||||
|
<td className="py-3 px-6">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${product.status === 1 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||||
|
{product.status === 1 ? 'Aktif' : 'Pasif'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-6 text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Link href={`/admin/products/${product.id}/edit`} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Düzenle">
|
||||||
|
<Pencil size={18} />
|
||||||
|
</Link>
|
||||||
|
<DeleteButton id={product.id} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-6 px-6 text-gray-500 text-sm">
|
||||||
|
Bu kategoride henüz ürün yok.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState, useState, useRef } from 'react'
|
||||||
|
import { saveSettings } from './actions'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { Upload, X, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function SettingsForm({
|
||||||
|
defaultRestaurantName,
|
||||||
|
defaultLocation,
|
||||||
|
defaultLogoUrl,
|
||||||
|
}: {
|
||||||
|
defaultRestaurantName: string
|
||||||
|
defaultLocation: string
|
||||||
|
defaultLogoUrl: string
|
||||||
|
}) {
|
||||||
|
const [state, action, isPending] = useActionState(saveSettings, undefined)
|
||||||
|
const [logoUrl, setLogoUrl] = useState(defaultLogoUrl)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const [uploadError, setUploadError] = useState('')
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
setUploadError("Dosya boyutu 5MB'ı geçemez.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadError('')
|
||||||
|
setUploading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
const res = await fetch('/api/upload', { method: 'POST', body: fd })
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Yükleme başarısız')
|
||||||
|
setLogoUrl(data.url)
|
||||||
|
} catch (err: any) {
|
||||||
|
setUploadError(err.message || 'Logo yüklenirken hata oluştu.')
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={action} className="space-y-6">
|
||||||
|
{state?.success && (
|
||||||
|
<div className="bg-green-50 text-green-700 p-3 rounded-lg text-sm">
|
||||||
|
{state.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{state?.error && (
|
||||||
|
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logo Upload */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Site Logosu
|
||||||
|
</label>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Preview */}
|
||||||
|
<div className="relative w-40 h-20 rounded-xl overflow-hidden border-2 border-gray-200 bg-stone-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
{logoUrl ? (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src={logoUrl}
|
||||||
|
alt="Logo önizleme"
|
||||||
|
fill
|
||||||
|
className="object-contain p-2"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setLogoUrl(''); if (fileInputRef.current) fileInputRef.current.value = '' }}
|
||||||
|
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-0.5 hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-400 text-center px-2">Logo yok<br />(varsayılan kullanılır)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload button */}
|
||||||
|
<div className="flex flex-col gap-2 justify-center h-20">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-600 hover:border-blue-400 hover:text-blue-600 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{uploading
|
||||||
|
? <><Loader2 size={16} className="animate-spin" /> Yükleniyor...</>
|
||||||
|
: <><Upload size={16} /> Logo Yükle</>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-gray-500">PNG, SVG, WEBP — Maks. 5MB</p>
|
||||||
|
{uploadError && <p className="text-xs text-red-600">{uploadError}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="logo_url" value={logoUrl} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-gray-100" />
|
||||||
|
|
||||||
|
{/* Restaurant Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Restoran / Mekan Adı
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="restaurant_name"
|
||||||
|
defaultValue={defaultRestaurantName}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Konum Bilgisi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="location"
|
||||||
|
defaultValue={defaultLocation}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending || uploading}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{isPending ? 'Kaydediliyor...' : 'Kaydet'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
|
export async function saveSettings(_prevState: unknown, formData: FormData) {
|
||||||
|
const restaurant_name = formData.get('restaurant_name') as string
|
||||||
|
const location = formData.get('location') as string
|
||||||
|
const logo_url = formData.get('logo_url') as string
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.settings.upsert({
|
||||||
|
where: { key: 'restaurant_name' },
|
||||||
|
update: { value: restaurant_name },
|
||||||
|
create: { key: 'restaurant_name', value: restaurant_name },
|
||||||
|
}),
|
||||||
|
prisma.settings.upsert({
|
||||||
|
where: { key: 'location' },
|
||||||
|
update: { value: location },
|
||||||
|
create: { key: 'location', value: location },
|
||||||
|
}),
|
||||||
|
prisma.settings.upsert({
|
||||||
|
where: { key: 'logo_url' },
|
||||||
|
update: { value: logo_url || '' },
|
||||||
|
create: { key: 'logo_url', value: logo_url || '' },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
revalidatePath('/')
|
||||||
|
revalidatePath('/admin/settings')
|
||||||
|
|
||||||
|
return { success: true, message: 'Ayarlar başarıyla kaydedildi.' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Settings save error:', error)
|
||||||
|
return { error: 'Ayarlar kaydedilirken bir hata oluştu.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import SettingsForm from './SettingsForm'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Genel Ayarlar - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SettingsPage() {
|
||||||
|
const settingsData = await prisma.settings.findMany()
|
||||||
|
const settings = settingsData.reduce((acc, curr) => {
|
||||||
|
acc[curr.key] = curr.value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
|
const restaurantName = settings['restaurant_name'] || 'Moy Beach'
|
||||||
|
const location = settings['location'] || 'Akyaka · Muğla'
|
||||||
|
const logoUrl = settings['logo_url'] || ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Genel Ayarlar</h1>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-2xl">
|
||||||
|
<SettingsForm
|
||||||
|
defaultRestaurantName={restaurantName}
|
||||||
|
defaultLocation={location}
|
||||||
|
defaultLogoUrl={logoUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { createUser } from './actions'
|
||||||
|
import { UserPlus } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function CreateUserForm() {
|
||||||
|
const [state, formAction, isPending] = useActionState(createUser, undefined)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<UserPlus size={20} className="text-blue-600" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">Yeni Kullanıcı Ekle</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state?.success && (
|
||||||
|
<div className="bg-green-50 text-green-700 p-3 rounded-lg text-sm mb-4">
|
||||||
|
Kullanıcı başarıyla oluşturuldu.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{state?.error && (
|
||||||
|
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm mb-4">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form action={formAction} className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kullanıcı Adı *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Şifre * <span className="text-gray-400 font-normal">(min. 6 karakter)</span></label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Rol</label>
|
||||||
|
<select
|
||||||
|
name="role"
|
||||||
|
defaultValue="admin"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
|
||||||
|
>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="editor">Editor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-3 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-5 rounded-lg text-sm transition-colors disabled:opacity-70 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<UserPlus size={16} />
|
||||||
|
{isPending ? 'Oluşturuluyor...' : 'Kullanıcı Oluştur'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState, useState } from 'react'
|
||||||
|
import { changePassword, deleteUser } from './actions'
|
||||||
|
import { KeyRound, Trash2, ChevronDown, ChevronUp, Shield } from 'lucide-react'
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
role: string
|
||||||
|
created_at: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangePasswordForm({ userId }: { userId: number }) {
|
||||||
|
const action = changePassword.bind(null, userId)
|
||||||
|
const [state, formAction, isPending] = useActionState(action, undefined)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={formAction} className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200 space-y-3">
|
||||||
|
{state?.success && (
|
||||||
|
<p className="text-xs text-green-700 bg-green-50 px-3 py-2 rounded-lg">{state.message}</p>
|
||||||
|
)}
|
||||||
|
{state?.error && (
|
||||||
|
<p className="text-xs text-red-700 bg-red-50 px-3 py-2 rounded-lg">{state.error}</p>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-600 mb-1">Yeni Şifre</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="new_password"
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="Min. 6 karakter"
|
||||||
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-600 mb-1">Şifre Tekrar</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirm_password"
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="Tekrar giriniz"
|
||||||
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium py-1.5 px-4 rounded-lg transition-colors disabled:opacity-70 flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<KeyRound size={14} />
|
||||||
|
{isPending ? 'Güncelleniyor...' : 'Şifreyi Güncelle'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserRow({ user }: { user: User }) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!confirm(`"${user.username}" kullanıcısını silmek istediğinize emin misiniz?`)) return
|
||||||
|
const res = await deleteUser(user.id)
|
||||||
|
if (res?.error) alert(res.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-gray-100 rounded-xl overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-semibold text-sm uppercase">
|
||||||
|
{user.username[0]}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900 text-sm">{user.username}</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
{new Date(user.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'
|
||||||
|
}`}>
|
||||||
|
<Shield size={10} />
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(v => !v)}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-amber-600 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<KeyRound size={13} />
|
||||||
|
Şifre
|
||||||
|
{expanded ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="Kullanıcıyı Sil"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && <ChangePasswordForm userId={user.id} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserList({ users }: { users: User[] }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">Mevcut Kullanıcılar ({users.length})</h2>
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-500 text-center py-6">Henüz kullanıcı yok.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{users.map(user => (
|
||||||
|
<UserRow key={user.id} user={user} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
|
export async function createUser(_prevState: unknown, formData: FormData) {
|
||||||
|
const username = (formData.get('username') as string)?.trim()
|
||||||
|
const password = formData.get('password') as string
|
||||||
|
const role = (formData.get('role') as string) || 'admin'
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return { error: 'Kullanıcı adı ve şifre zorunludur.' }
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
return { error: 'Şifre en az 6 karakter olmalıdır.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await prisma.users.findUnique({ where: { username } })
|
||||||
|
if (existing) {
|
||||||
|
return { error: 'Bu kullanıcı adı zaten kullanılıyor.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const password_hash = await bcrypt.hash(password, 12)
|
||||||
|
await prisma.users.create({ data: { username, password_hash, role } })
|
||||||
|
revalidatePath('/admin/users')
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create user error:', error)
|
||||||
|
return { error: 'Kullanıcı oluşturulurken hata oluştu.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(userId: number, _prevState: unknown, formData: FormData) {
|
||||||
|
const newPassword = formData.get('new_password') as string
|
||||||
|
const confirmPassword = formData.get('confirm_password') as string
|
||||||
|
|
||||||
|
if (!newPassword || newPassword.length < 6) {
|
||||||
|
return { error: 'Şifre en az 6 karakter olmalıdır.' }
|
||||||
|
}
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
return { error: 'Şifreler eşleşmiyor.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const password_hash = await bcrypt.hash(newPassword, 12)
|
||||||
|
await prisma.users.update({ where: { id: userId }, data: { password_hash } })
|
||||||
|
revalidatePath('/admin/users')
|
||||||
|
return { success: true, message: 'Şifre güncellendi.' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Change password error:', error)
|
||||||
|
return { error: 'Şifre güncellenirken hata oluştu.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(userId: number) {
|
||||||
|
try {
|
||||||
|
// Count total users — don't allow deleting the last admin
|
||||||
|
const count = await prisma.users.count()
|
||||||
|
if (count <= 1) {
|
||||||
|
return { error: 'Son yönetici kullanıcı silinemez.' }
|
||||||
|
}
|
||||||
|
await prisma.users.delete({ where: { id: userId } })
|
||||||
|
revalidatePath('/admin/users')
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete user error:', error)
|
||||||
|
return { error: 'Kullanıcı silinirken hata oluştu.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import CreateUserForm from './CreateUserForm'
|
||||||
|
import UserList from './UserList'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Kullanıcı Yönetimi - Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function UsersPage() {
|
||||||
|
const users = await prisma.users.findMany({
|
||||||
|
orderBy: { created_at: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
role: true,
|
||||||
|
created_at: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Kullanıcı Yönetimi</h1>
|
||||||
|
<CreateUserForm />
|
||||||
|
<UserList users={users} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { authenticate } from './actions'
|
||||||
|
|
||||||
|
export default function LoginForm() {
|
||||||
|
const [state, action, isPending] = useActionState(authenticate, undefined)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={action} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Kullanıcı Adı
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Şifre
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state?.error && (
|
||||||
|
<div className="text-red-500 text-sm bg-red-50 p-3 rounded-lg">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{isPending ? 'Giriş Yapılıyor...' : 'Giriş Yap'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { login } from '@/lib/auth'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export async function authenticate(prevState: any, formData: FormData) {
|
||||||
|
const username = formData.get('username') as string
|
||||||
|
const password = formData.get('password') as string
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return { error: 'Lütfen tüm alanları doldurun.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check user in database
|
||||||
|
const user = await prisma.users.findUnique({
|
||||||
|
where: { username }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { error: 'Kullanıcı adı veya şifre hatalı.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordsMatch = await bcrypt.compare(password, user.password_hash)
|
||||||
|
|
||||||
|
if (!passwordsMatch) {
|
||||||
|
return { error: 'Kullanıcı adı veya şifre hatalı.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await login(username)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return { error: 'Bir hata oluştu, lütfen tekrar deneyin.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to admin dashboard after successful login
|
||||||
|
redirect('/admin')
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
import LoginForm from './LoginForm'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Admin Login - Moy Beach',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function LoginPage() {
|
||||||
|
const session = await getSession()
|
||||||
|
if (session) {
|
||||||
|
redirect('/admin')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="max-w-md w-full p-8 bg-white rounded-xl shadow-lg">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Admin Girişi</h1>
|
||||||
|
<p className="text-gray-500 mt-2">Yönetim paneline erişmek için giriş yapın</p>
|
||||||
|
</div>
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import cloudinary from '@/lib/cloudinary'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const formData = await request.formData()
|
||||||
|
const file = formData.get('file') as File
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json({ error: 'Dosya bulunamadı.' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert file to buffer
|
||||||
|
const arrayBuffer = await file.arrayBuffer()
|
||||||
|
const buffer = Buffer.from(arrayBuffer)
|
||||||
|
const base64 = `data:${file.type};base64,${buffer.toString('base64')}`
|
||||||
|
|
||||||
|
// Upload to Cloudinary
|
||||||
|
const result = await cloudinary.uploader.upload(base64, {
|
||||||
|
folder: 'moy-qr/products',
|
||||||
|
transformation: [
|
||||||
|
{ width: 800, height: 800, crop: 'limit' },
|
||||||
|
{ quality: 'auto', fetch_format: 'auto' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ url: result.secure_url })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error)
|
||||||
|
return NextResponse.json({ error: 'Yükleme başarısız oldu.' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,70 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ── Design Tokens ─────────────────────────────── */
|
||||||
|
:root {
|
||||||
|
--cream: #FAF8F3;
|
||||||
|
--sand: #F0E8DC;
|
||||||
|
--sand-border: rgba(140, 108, 72, 0.13);
|
||||||
|
--amber: #C2712E;
|
||||||
|
--amber-light: #E49248;
|
||||||
|
--amber-pale: #F6EDDE;
|
||||||
|
--stone-ink: #2E2820;
|
||||||
|
--stone-mid: #6B5E54;
|
||||||
|
--stone-muted: #9C9088;
|
||||||
|
--hero-from: #1E0E06;
|
||||||
|
--hero-via: #7C3312;
|
||||||
|
--hero-to: #C96B28;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-cream: var(--cream);
|
||||||
|
--color-sand: var(--sand);
|
||||||
|
--color-amber: var(--amber);
|
||||||
|
--color-amber-light: var(--amber-light);
|
||||||
|
--color-amber-pale: var(--amber-pale);
|
||||||
|
--color-stone-ink: var(--stone-ink);
|
||||||
|
--color-stone-mid: var(--stone-mid);
|
||||||
|
--color-stone-muted: var(--stone-muted);
|
||||||
|
|
||||||
|
--font-display: var(--font-cormorant);
|
||||||
|
--font-sans: var(--font-jakarta);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--cream);
|
||||||
|
color: var(--stone-ink);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-display {
|
||||||
|
font-family: var(--font-cormorant), "Garamond", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
170deg,
|
||||||
|
var(--hero-from) 0%,
|
||||||
|
var(--hero-via) 50%,
|
||||||
|
var(--hero-to) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-active {
|
||||||
|
background: linear-gradient(135deg, #C2712E, #9A5320);
|
||||||
|
color: #FFF6E8;
|
||||||
|
box-shadow: 0 2px 10px rgba(194, 113, 46, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-inactive {
|
||||||
|
background: rgba(120, 90, 58, 0.09);
|
||||||
|
color: var(--stone-mid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Cormorant_Garamond, Plus_Jakarta_Sans } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const cormorant = Cormorant_Garamond({
|
||||||
|
variable: "--font-cormorant",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700"],
|
||||||
|
style: ["normal", "italic"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const jakarta = Plus_Jakarta_Sans({
|
||||||
|
variable: "--font-jakarta",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Moy Beach Akyaka | Menü",
|
||||||
|
description: "Moy Beach Akyaka dijital menü.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="tr" className={`${cormorant.variable} ${jakarta.variable} h-full`}>
|
||||||
|
<body className="min-h-full flex flex-col antialiased">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import MenuClient from "./MenuClient";
|
||||||
|
import { MenuCategory } from "@/data/menu";
|
||||||
|
|
||||||
|
async function getMenuData(): Promise<MenuCategory[]> {
|
||||||
|
const dbCategories = await prisma.categories.findMany({
|
||||||
|
where: { status: 1 },
|
||||||
|
orderBy: { order_num: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbProducts = await prisma.products.findMany({
|
||||||
|
where: { status: 1 },
|
||||||
|
orderBy: [{ category_id: "asc" }, { id: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return dbCategories.map((cat) => {
|
||||||
|
const categoryProducts = dbProducts.filter((p) => p.category_id === cat.id);
|
||||||
|
return {
|
||||||
|
id: cat.slug,
|
||||||
|
title: cat.name_tr,
|
||||||
|
items: categoryProducts.map((p) => ({
|
||||||
|
id: p.id.toString(),
|
||||||
|
name: p.name_tr,
|
||||||
|
description: p.description_tr,
|
||||||
|
price: p.price.toString() + " ₺",
|
||||||
|
image: p.image_url || '/default-product.png',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSiteSettings() {
|
||||||
|
const settingsData = await prisma.settings.findMany()
|
||||||
|
const settings = settingsData.reduce((acc, curr) => {
|
||||||
|
acc[curr.key] = curr.value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
|
return {
|
||||||
|
logoUrl: settings['logo_url'] || '/logo.png',
|
||||||
|
location: settings['location'] || 'Akyaka · Muğla',
|
||||||
|
restaurantName: settings['restaurant_name'] || 'Moy Beach',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const [menuData, siteSettings] = await Promise.all([
|
||||||
|
getMenuData(),
|
||||||
|
getSiteSettings(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <MenuClient initialCategories={menuData} siteSettings={siteSettings} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { MenuCategory } from "@/data/menu";
|
||||||
|
|
||||||
|
interface CategoryNavProps {
|
||||||
|
categories: MenuCategory[];
|
||||||
|
activeCategoryId: string;
|
||||||
|
icons: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategoryNav({ categories, activeCategoryId, icons }: CategoryNavProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeCategoryId || !containerRef.current) return;
|
||||||
|
const el = containerRef.current.querySelector(
|
||||||
|
`[data-id="${activeCategoryId}"]`
|
||||||
|
) as HTMLElement | null;
|
||||||
|
el?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
||||||
|
}, [activeCategoryId]);
|
||||||
|
|
||||||
|
const scrollTo = (id: string) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return;
|
||||||
|
const y = el.getBoundingClientRect().top + window.scrollY - 72;
|
||||||
|
window.scrollTo({ top: y, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="sticky top-0 z-50 border-b"
|
||||||
|
style={{
|
||||||
|
background: "rgba(250, 248, 243, 0.93)",
|
||||||
|
backdropFilter: "blur(18px)",
|
||||||
|
WebkitBackdropFilter: "blur(18px)",
|
||||||
|
borderColor: "rgba(140, 108, 72, 0.12)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="flex overflow-x-auto hide-scrollbar px-3 py-2 gap-1.5 items-center"
|
||||||
|
>
|
||||||
|
{categories.map((cat) => {
|
||||||
|
const active = cat.id === activeCategoryId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
data-id={cat.id}
|
||||||
|
onClick={() => scrollTo(cat.id)}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-1.5 whitespace-nowrap px-3 py-1.5 rounded-full
|
||||||
|
text-[11px] font-medium font-sans shrink-0
|
||||||
|
transition-all duration-200 active:scale-95
|
||||||
|
${active ? "pill-active" : "pill-inactive hover:bg-[rgba(120,90,58,0.14)]"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<span className="text-[13px] leading-none select-none">
|
||||||
|
{icons[cat.id] ?? "🍽️"}
|
||||||
|
</span>
|
||||||
|
<span>{cat.title}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { MenuItem as MenuItemType } from "@/data/menu";
|
||||||
|
|
||||||
|
export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () => void }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="py-3.5 border-b last:border-b-0 cursor-pointer transition-colors hover:bg-black/5"
|
||||||
|
style={{ borderColor: "rgba(140, 108, 72, 0.08)" }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3
|
||||||
|
className="text-[13.5px] font-semibold leading-snug font-sans"
|
||||||
|
style={{ color: "var(--stone-ink)" }}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</h3>
|
||||||
|
{item.description && (
|
||||||
|
<p
|
||||||
|
className="mt-0.5 text-[11.5px] leading-relaxed font-sans line-clamp-2"
|
||||||
|
style={{ color: "var(--stone-muted)" }}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{item.price && (
|
||||||
|
<div
|
||||||
|
className="mt-1 text-[13px] font-semibold font-sans tabular-nums"
|
||||||
|
style={{ color: "var(--amber)" }}
|
||||||
|
>
|
||||||
|
{item.price}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{item.image && (
|
||||||
|
<div className="shrink-0">
|
||||||
|
<img
|
||||||
|
src={item.image}
|
||||||
|
alt={item.name}
|
||||||
|
className="w-20 h-20 object-cover rounded-lg shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Moy Beach Akyaka — QR Dijital Menü Yenileme Prompt'u
|
||||||
|
|
||||||
|
## Proje Tanımı
|
||||||
|
|
||||||
|
**URL:** menu.moybeachakyaka.com
|
||||||
|
**Tip:** QR kod ile masadan erişilen tek sayfalık dijital menü
|
||||||
|
**Dil:** Türkçe + İngilizce
|
||||||
|
**Kullanıcı:** Masada oturan misafir, telefonuyla QR okutarak erişiyor
|
||||||
|
|
||||||
|
Mevcut QR menü sitesini kullanım kolaylığı, görsel kalite ve marka tutarlılığı
|
||||||
|
açısından yenile. Site bir e-ticaret ya da rezervasyon platformu değil;
|
||||||
|
**sadece menüyü hızlı ve rahat gösteren bir dijital liste.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mevcut Sorunlar
|
||||||
|
|
||||||
|
**Navigasyon**
|
||||||
|
- Kategori yok, kullanıcı sayfayı sonuna kadar kaydırmak zorunda
|
||||||
|
- Telefonda çok uzun bir scroll deneyimi oluşuyor
|
||||||
|
|
||||||
|
**Görsel**
|
||||||
|
- Hiçbir üründe fotoğraf yok, hepsi aynı generik çatal-kaşık ikonu gösteriyor
|
||||||
|
- Ürünler birbirinden görsel olarak ayrışmıyor
|
||||||
|
|
||||||
|
**İçerik Tutarsızlıkları**
|
||||||
|
- Bazı ürün açıklamaları İngilizce kalmış
|
||||||
|
(örn. Roka Salatası: *"Pink Tomatoes, Arugula, Parmesan and Balsamic Glaze"*)
|
||||||
|
- Şişe içeceklerin tamamında fiyat **₺0,00** görünüyor
|
||||||
|
- Stella Artois birasının açıklaması hiç yok
|
||||||
|
|
||||||
|
**Mobil Deneyim**
|
||||||
|
- 3'lü grid layout dar telefonlarda okunaksız hale geliyor
|
||||||
|
- Ürün adları ve açıklamalar kırpılıyor (`...` ile bitiyor), tam metin görülemiyor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## İstenen İyileştirmeler
|
||||||
|
|
||||||
|
### 1. Kategori Navigasyonu
|
||||||
|
|
||||||
|
Sayfanın üstüne sabit (sticky) yatay kaydırılabilir bir kategori çubuğu ekle.
|
||||||
|
Tıklandığında ilgili bölüme smooth scroll ile git.
|
||||||
|
|
||||||
|
Kategoriler (mevcut sırayla):
|
||||||
@@ -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,79 @@
|
|||||||
|
import { SignJWT, jwtVerify } from 'jose'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const secretKey = process.env.JWT_SECRET || 'fallback-secret-key-do-not-use-in-prod'
|
||||||
|
const key = new TextEncoder().encode(secretKey)
|
||||||
|
|
||||||
|
export async function encrypt(payload: any) {
|
||||||
|
return await new SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('24h')
|
||||||
|
.sign(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(input: string): Promise<any> {
|
||||||
|
const { payload } = await jwtVerify(input, key, {
|
||||||
|
algorithms: ['HS256'],
|
||||||
|
})
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(username: string) {
|
||||||
|
// Create the session
|
||||||
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||||
|
const session = await encrypt({ username, expires })
|
||||||
|
|
||||||
|
// Save the session in a cookie
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
cookieStore.set('session', session, {
|
||||||
|
expires,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
cookieStore.set('session', '', {
|
||||||
|
expires: new Date(0),
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSession() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
const session = cookieStore.get('session')?.value
|
||||||
|
if (!session) return null
|
||||||
|
try {
|
||||||
|
return await decrypt(session)
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSession(request: NextRequest) {
|
||||||
|
const session = request.cookies.get('session')?.value
|
||||||
|
if (!session) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = await decrypt(session)
|
||||||
|
parsed.expires = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||||
|
const res = NextResponse.next()
|
||||||
|
res.cookies.set({
|
||||||
|
name: 'session',
|
||||||
|
value: await encrypt(parsed),
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
expires: parsed.expires,
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { v2 as cloudinary } from 'cloudinary'
|
||||||
|
|
||||||
|
cloudinary.config({
|
||||||
|
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
|
||||||
|
api_key: process.env.CLOUDINARY_API_KEY,
|
||||||
|
api_secret: process.env.CLOUDINARY_API_SECRET,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default cloudinary
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { Pool } from 'pg'
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg'
|
||||||
|
|
||||||
|
const prismaClientSingleton = () => {
|
||||||
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
|
||||||
|
const adapter = new PrismaPg(pool)
|
||||||
|
return new PrismaClient({ adapter })
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const globalThis: {
|
||||||
|
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
||||||
|
} & typeof global;
|
||||||
|
|
||||||
|
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||||
|
|
||||||
|
export default prisma
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'images.unsplash.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'res.cloudinary.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
/* other config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "moy-qr",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
|
"@prisma/client": "^7.8.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"cloudinary": "^2.10.0",
|
||||||
|
"framer-motion": "^12.40.0",
|
||||||
|
"jose": "^6.2.3",
|
||||||
|
"lucide-react": "^1.17.0",
|
||||||
|
"next": "16.2.7",
|
||||||
|
"next-cloudinary": "^6.17.5",
|
||||||
|
"pg": "^8.21.0",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.2.7",
|
||||||
|
"prisma": "^7.8.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
|
// npm install --save-dev prisma dotenv
|
||||||
|
import "dotenv/config";
|
||||||
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: process.env["DATABASE_URL"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
model categories {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
name_tr String @db.Text
|
||||||
|
slug String @db.VarChar(100)
|
||||||
|
order_num Int? @default(0)
|
||||||
|
status Int? @default(1) @db.SmallInt
|
||||||
|
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||||
|
products products[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model products {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
category_id Int?
|
||||||
|
category categories? @relation(fields: [category_id], references: [id])
|
||||||
|
name String @db.VarChar(255)
|
||||||
|
name_tr String @db.VarChar(255)
|
||||||
|
slug String @db.VarChar(255)
|
||||||
|
short_description String? @db.VarChar(255)
|
||||||
|
description String? @db.Text
|
||||||
|
description_tr String @db.Text
|
||||||
|
price Decimal @db.Decimal(10, 2)
|
||||||
|
image_url String? @db.VarChar(255)
|
||||||
|
status Int? @default(1) @db.SmallInt
|
||||||
|
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||||
|
updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model users {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
username String @unique @db.VarChar(50)
|
||||||
|
password_hash String @db.VarChar(255)
|
||||||
|
role String @default("admin") @db.VarChar(50)
|
||||||
|
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||||
|
updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model settings {
|
||||||
|
key String @id @db.VarChar(100)
|
||||||
|
value String @db.Text
|
||||||
|
updated_at DateTime @default(now()) @updatedAt @db.Timestamp(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { updateSession, decrypt } from './lib/auth'
|
||||||
|
|
||||||
|
export async function proxy(request: NextRequest) {
|
||||||
|
// Always update session expiration
|
||||||
|
let response = await updateSession(request)
|
||||||
|
|
||||||
|
const isAuthPage = request.nextUrl.pathname.startsWith('/admin/login')
|
||||||
|
const isAdminPage = request.nextUrl.pathname.startsWith('/admin')
|
||||||
|
|
||||||
|
// Check if session exists and is valid
|
||||||
|
const sessionValue = request.cookies.get('session')?.value
|
||||||
|
let session = null
|
||||||
|
if (sessionValue) {
|
||||||
|
try {
|
||||||
|
session = await decrypt(sessionValue)
|
||||||
|
} catch(e) {
|
||||||
|
session = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trying to access admin pages (except login) without a valid session
|
||||||
|
if (isAdminPage && !isAuthPage && !session) {
|
||||||
|
return NextResponse.redirect(new URL('/admin/login', request.url))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trying to access login page with a valid session
|
||||||
|
if (isAuthPage && session) {
|
||||||
|
return NextResponse.redirect(new URL('/admin', request.url))
|
||||||
|
}
|
||||||
|
|
||||||
|
return response || NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/admin/:path*'],
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 197 KiB |
@@ -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 |
|
After Width: | Height: | Size: 919 KiB |
|
After Width: | Height: | Size: 63 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,39 @@
|
|||||||
|
import { Client } from 'pg';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const username = 'admin';
|
||||||
|
const password = 'password123'; // Users should change this!
|
||||||
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if user already exists
|
||||||
|
const res = await client.query('SELECT id FROM users WHERE username = $1', [username]);
|
||||||
|
if (res.rowCount && res.rowCount > 0) {
|
||||||
|
console.log('Admin user already exists.');
|
||||||
|
} else {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW())',
|
||||||
|
[username, passwordHash, 'admin']
|
||||||
|
);
|
||||||
|
console.log('Admin user created successfully.');
|
||||||
|
console.log(`Username: ${username}`);
|
||||||
|
console.log(`Password: ${password}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating admin user:', error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
-- phpMyAdmin SQL Dump
|
||||||
|
-- version 5.2.2
|
||||||
|
-- https://www.phpmyadmin.net/
|
||||||
|
--
|
||||||
|
-- Anamakine: localhost:3306
|
||||||
|
-- Üretim Zamanı: 11 Haz 2026, 13:22:32
|
||||||
|
-- Sunucu sürümü: 11.4.12-MariaDB
|
||||||
|
-- PHP Sürümü: 8.4.21
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+00:00";
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Veritabanı: `kitebeac_menu`
|
||||||
|
--
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için tablo yapısı `banners`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `banners` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`image_url` varchar(255) NOT NULL,
|
||||||
|
`title` varchar(255) DEFAULT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`order_num` int(11) DEFAULT 0,
|
||||||
|
`status` tinyint(1) DEFAULT 1,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_turkish_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo döküm verisi `banners`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `banners` (`id`, `image_url`, `title`, `description`, `order_num`, `status`, `created_at`) VALUES
|
||||||
|
(12, 'admin/uploads/banners/6821f7a55d765_1747056549.jpg', '', '', 0, 1, '2025-03-14 15:55:16');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için tablo yapısı `categories`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `categories` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`name_tr` text NOT NULL,
|
||||||
|
`slug` varchar(100) NOT NULL,
|
||||||
|
`order_num` int(11) DEFAULT 0,
|
||||||
|
`status` tinyint(1) DEFAULT 1,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_turkish_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo döküm verisi `categories`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `categories` (`id`, `name`, `name_tr`, `slug`, `order_num`, `status`, `created_at`) VALUES
|
||||||
|
(2, 'Pastas', 'Makarnalar', 'pastas', 6, 1, '2024-12-30 18:47:43'),
|
||||||
|
(10, 'Pizzas', 'Pizzalar', 'pizzas', 5, 1, '2025-03-14 15:42:28'),
|
||||||
|
(13, 'Breakfast', 'Kahvaltı', 'breakfast', 0, 1, '2025-03-14 15:43:16'),
|
||||||
|
(14, 'Brunch', 'Güne Başlarken', 'brunch', 1, 1, '2025-03-14 15:44:21'),
|
||||||
|
(16, 'Main Courses From The Grill', 'Izgaradan Ana Yemekler', 'main-courses-from-the-grill', 8, 1, '2025-03-14 15:44:37'),
|
||||||
|
(17, 'Burgers & Sandwiches', 'Burgerler&Sandviçler', 'burgers-&-sandwiches', 7, 1, '2025-03-14 15:44:46'),
|
||||||
|
(18, 'Salads', 'Salatalar', 'salads', 4, 1, '2025-03-14 15:44:52'),
|
||||||
|
(24, 'Hot Drinks', 'Sıcak Kahve Çeşitleri', 'hot-drinks', 20, 1, '2025-04-30 18:13:54'),
|
||||||
|
(25, 'Cold Drinks', 'Soğuk İçecekler', 'cold-drinks', 10, 1, '2025-04-30 18:14:04'),
|
||||||
|
(26, 'Mix Coctails', 'Mix Kokteyller', 'mix-coctails', 13, 1, '2025-05-05 12:43:03'),
|
||||||
|
(27, 'Classic Cocktails', 'Klasik Kokteyller', 'classic-cocktails', 12, 1, '2025-05-05 13:14:04'),
|
||||||
|
(28, 'Beers', 'Biralar', 'beers', 14, 1, '2025-05-05 13:25:02'),
|
||||||
|
(29, 'Snack & Appetızersss', 'Atıştırmalıklar', 'snack-&-appetızersss', 11, 1, '2025-05-09 11:01:42'),
|
||||||
|
(30, 'Cold Coffes', 'Soğuk Kahve Çeşitleri', 'cold-coffes', 19, 1, '2025-05-11 11:49:06'),
|
||||||
|
(31, 'Shot', 'Shot', 'shot', 15, 1, '2025-06-01 09:32:31'),
|
||||||
|
(32, 'Alcohols', 'Alkollü İçecekler', 'alcohols', 21, 1, '2025-06-01 09:51:20'),
|
||||||
|
(34, 'Snack & Appetızers', 'Atıştırmalıklar', 'snack-&-appetızers', 3, 1, '2026-05-24 14:25:17');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için tablo yapısı `products`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `products` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`category_id` int(11) DEFAULT NULL,
|
||||||
|
`name` varchar(255) NOT NULL,
|
||||||
|
`name_tr` varchar(255) NOT NULL,
|
||||||
|
`slug` varchar(255) NOT NULL,
|
||||||
|
`short_description` varchar(255) DEFAULT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`description_tr` text NOT NULL,
|
||||||
|
`price` decimal(10,2) NOT NULL,
|
||||||
|
`image_url` varchar(255) DEFAULT NULL,
|
||||||
|
`status` tinyint(1) DEFAULT 1,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_turkish_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo döküm verisi `products`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `products` (`id`, `category_id`, `name`, `name_tr`, `slug`, `short_description`, `description`, `description_tr`, `price`, `image_url`, `status`, `created_at`, `updated_at`) VALUES
|
||||||
|
(419, 14, 'Menemen', 'Menemen', 'menemen', '', '', '', 550.00, 'admin/uploads/products/67e5812d489b2_1743094061.jpg', 1, '2025-03-19 11:20:09', '2026-06-02 12:46:05'),
|
||||||
|
(420, 14, 'Omelets', 'Omlet', 'omelets', '', '', '', 450.00, 'admin/uploads/products/67e58130b43b8_1743094064.jpg', 1, '2025-03-19 11:20:09', '2026-06-02 12:46:22'),
|
||||||
|
(421, 14, 'Avocado Toast', 'Avokado Tost', 'avocado-toast', '', '(Ekmek Üstü Avokado, Suda Mozzarella, Pesto Sos, Füme Kaburga, çırpılmış yumurta ile)(Buffalo Mozzarella Cheese, Dry Beef Rib, scramble Egg, Pesto Sauce)', '', 1100.00, 'admin/uploads/products/683ee7dfc9419_1748953055.jpeg', 1, '2025-03-19 11:20:09', '2026-06-02 12:51:29'),
|
||||||
|
(422, 16, 'Beetroot Toast', 'Pancar Tost', 'beetroot-toast', '', '(Ekmek Üstü Pancar, Keçi Peyniri, Hardal Sos, Füme Kaburga, Çırpılmış Yumurta ile)(Beetroot, Goat Cheese, Smoked Rib, Scramble Egg, Mustard Sauce)', '', 800.00, 'admin/uploads/products/67e5834f725f9_1743094607.jpg', 0, '2025-03-19 11:20:09', '2026-05-26 12:33:40'),
|
||||||
|
(423, 14, 'Granola Bowls with Forrest Fruits', 'Granola Kasesi', 'granola-bowls-with-forrest-fruits', '', '', '', 850.00, 'admin/uploads/products/67e5837a38c39_1743094650.jpg', 1, '2025-03-19 11:20:09', '2026-06-04 22:56:17'),
|
||||||
|
(424, 14, 'Antipasti Plate', 'Antipasti Tabağı', 'antipasti-plate', '', 'Parmesan Peyniri(Parmesan), Edam Peyniri(Edam), Kars Gravyeri(Kars Gravyer), Bergama Tulum, Kuru Et Çubukları(Dry Beef Sticks), İtalyan Salam( Italian Salam), Kayseri Pastırma, Füme Dana Kaburga(Smoked Beef Rib), Kuru Meyveler(Dry Fruits)', '', 1100.00, 'admin/uploads/products/6847dcfc48c4c_1749540092.jpeg', 0, '2025-03-19 11:20:09', '2025-09-13 11:24:23'),
|
||||||
|
(430, 29, 'Avocado Bowl', 'Avokado Kasesi', 'avocado-bowl', '', 'Bebek Mısırlar(Baby Corn), Enginar(Artichoke), Edamame(), Ezine Peyniri(Ezine Cheese), Çeri Domatesler(Cherry Tomatoes)', '', 900.00, 'admin/uploads/products/683ef3b663f12_1748956086.jpeg', 0, '2025-03-19 11:20:09', '2026-05-26 12:26:47'),
|
||||||
|
(432, 2, 'Linguine Spicy Cream', 'Acı Kremalı Linguine', 'linguine-spicy-cream', '', '(Acılı domates soslu) kremalı) (With spicy tomato sauce)', '', 800.00, 'admin/uploads/products/6a182e22b130d_1779969570.png', 1, '2025-03-19 11:20:09', '2026-05-28 11:59:30'),
|
||||||
|
(433, 2, 'Linguine Napolitan', 'Linguine Napolitan', 'linguine-napolitan', '', 'Napolitan Domates Soslu)(Napolitan Tomato Sauce)', '', 700.00, 'admin/uploads/products/67e5847c0d4c4_1743094908.jpg', 1, '2025-03-19 11:20:09', '2026-05-26 12:32:47'),
|
||||||
|
(434, 2, 'Fettucine Alfredo with Chicken adn Mushroom', 'Tagliatelle Alfredo mantar ve tavuklu', 'fettucine-alfredo-with-chicken-adn-mushroom', '', '(Kremalı, Mantarlı, Tavuklu)(Creamy, Mushroom, Chicken)', '', 950.00, 'admin/uploads/products/67e5858485bac_1743095172.jpg', 1, '2025-03-19 11:20:09', '2026-05-26 12:37:55'),
|
||||||
|
(435, 2, 'Linguini Di Mare', 'Taglietelle Di Mare', 'linguini-di-mare', '', '(Karides ve Kremalı)(Shrimp and Cream)', '', 1200.00, 'admin/uploads/products/683ef6921b243_1748956818.jpeg', 0, '2025-03-19 11:20:09', '2026-05-26 12:20:19'),
|
||||||
|
(436, 2, 'Turkish Steaming Dumpling', 'Mantı', 'turkish-steaming-dumpling', '', '(Sarımsak Yoğurtlu)(Garlic Yogurt)', '', 800.00, 'admin/uploads/products/67e580b22b1c8_1743093938.jpg', 0, '2025-03-19 11:20:09', '2026-05-26 12:20:45'),
|
||||||
|
(438, 10, 'Pizza Margherita', 'Margarita Pizza', 'pizza-margherita', '', '', '', 750.00, 'admin/uploads/products/67e581ec4beb3_1743094252.jpg', 1, '2025-03-19 11:20:09', '2026-05-24 13:51:04'),
|
||||||
|
(439, 10, 'Feta Cheese', 'Ezine Peynirli Pizza', 'feta-cheese', '', '', 'Pizza sosu, mozzarella peyniri, ezine peyniri, pesto sos', 850.00, 'admin/uploads/products/67e5af228253f_1743105826.jpg', 1, '2025-03-19 11:20:09', '2026-06-02 12:42:21'),
|
||||||
|
(440, 10, 'Pepperoni Pizza', 'Sucuklu Pizza', 'pepperoni-pizza', '', '', '', 1000.00, 'admin/uploads/products/67e581cc20110_1743094220.jpg', 1, '2025-03-19 11:20:09', '2026-05-24 13:51:43'),
|
||||||
|
(441, 10, 'Vegeterian', 'Vejeteryan', 'vegeterian', '', '', '', 850.00, 'admin/uploads/products/6a130430c8c48_1779631152.png', 1, '2025-03-19 11:20:09', '2026-05-24 13:59:12'),
|
||||||
|
(442, 10, 'Mixed Pizza', 'Karışık Pizza', 'mixed-pizza', '', '', '', 1100.00, 'admin/uploads/products/67e580f499bc3_1743094004.jpg', 1, '2025-03-19 11:20:09', '2025-05-07 18:20:18'),
|
||||||
|
(443, 10, 'Truffle Pizza', 'Trüflü Pizza', 'truffle-pizza', '', '', '', 1150.00, 'admin/uploads/products/6a130372a42a3_1779630962.png', 1, '2025-03-19 11:20:09', '2026-05-24 13:56:02'),
|
||||||
|
(444, 10, 'Smoked Beef Rib Pizza', 'İsli Kaburgalı Pizza', 'smoked-beef-rib-pizza', '', '', '', 1350.00, 'admin/uploads/products/6a130501ac696_1779631361.png', 1, '2025-03-19 11:20:09', '2026-05-28 10:43:55'),
|
||||||
|
(445, 17, 'Cheeseburger', 'Cheeseburger', 'cheeseburger', '', '(Biftek Patates Kızartması, Trüflü Mayonez)(Steak Fries, Truffle Mayo)', '', 1150.00, 'admin/uploads/products/6867b72eb031c_1751627566.jpeg', 1, '2025-03-19 11:20:09', '2026-05-28 09:48:04'),
|
||||||
|
(446, 16, 'Trio Miniburger', 'Mini Burger Üçlemesi', 'trio-miniburger', '', '(Biftek Patates Kızartması, Trüflü Mayonez)(Steak Fries, Truffle Mayo)', '', 980.00, 'admin/uploads/products/67e5afaede162_1743105966.jpg', 0, '2025-03-19 11:20:09', '2026-05-26 12:21:22'),
|
||||||
|
(447, 16, 'Bao-Bun (Seabass Or Beef)', 'Bao-Bun (Levrek Ya Da Dana Bonfile)', 'bao-bun-(seabass-or-beef)', '', '(Seabass or Beef) (Levrek Tempura ya da İsli Bonfile ile)', '', 900.00, 'admin/uploads/products/683f007865bab_1748959352.jpeg', 0, '2025-03-19 11:20:09', '2025-09-13 11:27:51'),
|
||||||
|
(451, 17, 'Artichoke Tempura', 'Çıtır Enginar', 'artichoke-tempura', '', '(Tempura Roma Tipi Enginar, Sakız Kabak Püresi, Çıtır Soğanlar, Kırmızı Soğan ile)(Basil Zuchini Paste, Crispy Onion, Truffle Mayo)', '', 700.00, 'admin/uploads/products/683ef4bea8c58_1748956350.jpeg', 0, '2025-03-19 11:20:09', '2026-05-24 14:07:14'),
|
||||||
|
(452, 17, 'Tempura Shrimp Wasabi', 'Çıtır Karides Wasabi', 'tempura-shrimp-wasabi', '', '(Tempura Karides, Wasabi Mayo, Limon Jel, Tobiko ile)(Wasabi Mayo and Lemon Gel)', '', 980.00, 'admin/uploads/products/683ef6efb8974_1748956911.jpeg', 0, '2025-03-19 11:20:09', '2026-05-24 14:06:49'),
|
||||||
|
(453, 34, 'Deep Fried Mixed Plate', 'Karışık Çıtır Tabağı', 'deep-fried-mixed-plate', '', '(Çıtır Patates, Parmak Tavuk, Soğan Halkası, Sosis)', '', 1200.00, 'admin/uploads/products/6a130bda2105e_1779633114.png', 0, '2025-03-19 11:20:09', '2026-05-26 11:17:18'),
|
||||||
|
(455, 34, 'Truffle Fries', 'Trüf Patates Kızartması', 'truffle-fries', '', '', '', 750.00, 'admin/uploads/products/67e585c367ee3_1743095235.jpg', 1, '2025-03-19 11:20:09', '2026-05-24 14:28:20'),
|
||||||
|
(456, 18, 'Rocket Salad', 'Roka Salatası', 'rocket-salad', '', 'Roka(Rocket), Balzemik Sirke(Sauce), Ceviz(Walnuts), Parmesan(Parmesan Cheese), Çeri Domates(Cherry Tomatoes)', '', 750.00, 'admin/uploads/products/6a1307444196e_1779631940.png', 0, '2025-03-19 11:20:09', '2026-05-26 12:29:53'),
|
||||||
|
(457, 18, 'Greek Salad', 'Yunan Salatası', 'greek-salad', '', '', '', 750.00, 'admin/uploads/products/67e582b1cfc31_1743094449.jpg', 0, '2025-03-19 11:20:09', '2026-05-26 12:28:37'),
|
||||||
|
(461, 16, 'Grilled Chicken', 'Tavuk Izgara', 'grilled-chicken', '', '(Tereyağlı Pide, Sumaklı Soğan, Patates Kızartması, Sarımsaklı Yoğurt)(Traditional Turkish)', '', 900.00, 'admin/uploads/products/6a15939747329_1779798935.png', 1, '2025-03-19 11:20:09', '2026-05-26 12:35:35'),
|
||||||
|
(462, 16, 'Grilled Meatball', 'Izgara Kasap Köfte', 'grilled-meatball', '', '', '', 1000.00, 'admin/uploads/products/67e57c016d666_1743092737.jpg', 1, '2025-03-19 11:20:09', '2026-05-26 12:00:55'),
|
||||||
|
(464, 16, 'Chimichurri Beef Skewers', 'Chimichurri Bonfile Şişleri', 'chimichurri-beef-skewers', '', '(Izgara Bonfile Şişleri, Chimichurri Sos, Bebek Mısırlar, Enginar Püresi, Tatlı Rokata Sos)(Chimichurri Sauce, Baby Corn, Artichoke Paste, Sweet Rokata Sauce)', '', 1200.00, 'admin/uploads/products/6847daa6d1d39_1749539494.jpeg', 0, '2025-03-19 11:20:09', '2026-05-26 12:25:54'),
|
||||||
|
(465, 16, 'Dry Age Rib-Eye Steak', 'Antrikot Izgara', 'dry-age-rib-eye-steak', '', '', 'Dana antrikot, parmesanlı patates püresi, Balzamik soslu Akdeniz salatası, ', 1200.00, 'admin/uploads/products/6a22047727c3b_1780614263.jpg', 0, '2025-03-19 11:20:09', '2026-06-04 23:04:23'),
|
||||||
|
(466, 16, 'Grilled Seabass 250 gr', 'Izgara Levrek', 'grilled-seabass-250-gr', '', '(Izgara Takoz Levrek 250 gr, Bebek Sebzeler, Pancar Püresi, Limon Jel, Kuşkonmaz)(Baby Vegetables, Beetroot Paste, Lemon Gel, Asparagus)', '', 1100.00, 'admin/uploads/products/683ef0fdd66a5_1748955389.jpeg', 0, '2025-03-19 11:20:09', '2026-05-26 12:13:46'),
|
||||||
|
(468, 12, 'French Souffle with Vanilla Ice Cream', 'Fransız Sufle', 'french-souffle-with-vanilla-ice-cream', '', '', '', 600.00, 'admin/uploads/products/683ee88d0cfa0_1748953229.jpeg', 0, '2025-03-19 11:20:09', '2025-09-13 11:27:27'),
|
||||||
|
(469, 12, 'Turkish Traditional Sütlaç', 'Geleneksel Sütlaç', 'turkish-traditional-sütlaç', '', '', '', 500.00, 'admin/uploads/products/67e57d55e5baf_1743093077.jpg', 0, '2025-03-19 11:20:09', '2025-09-13 11:27:04'),
|
||||||
|
(475, 34, 'French Fries', 'Patates Kızartması', 'french-fries', '', '', '', 600.00, 'admin/uploads/products/68126eaef16b6_1746038446.jpg', 1, '2025-04-30 18:40:46', '2026-05-24 14:27:59'),
|
||||||
|
(476, 24, 'Espresso Single', 'Espresso', 'espresso-single', '', '', '', 280.00, 'admin/uploads/products/6813782567b88_1746106405.jpg', 1, '2025-05-01 13:33:25', '2026-05-27 09:32:30'),
|
||||||
|
(477, 24, 'Espresso Double', 'Espresso Double', 'espresso-double', '', '', '', 295.00, 'admin/uploads/products/6813784d3bc92_1746106445.jpg', 1, '2025-05-01 13:34:05', '2026-05-27 09:28:54'),
|
||||||
|
(478, 24, 'Americano', 'Americano', 'americano', '', '', '', 325.00, 'admin/uploads/products/68137a6123153_1746106977.jpg', 1, '2025-05-01 13:42:57', '2026-05-27 09:14:20'),
|
||||||
|
(481, 24, 'Cortado', 'Cortado', 'cortado', '', '', '', 350.00, 'admin/uploads/products/68137ad61ea36_1746107094.jpg', 1, '2025-05-01 13:44:54', '2026-05-27 09:17:27'),
|
||||||
|
(482, 24, 'Flat White', 'Flat White', 'flat-white', '', '', '', 350.00, 'admin/uploads/products/68137af63b98f_1746107126.jpg', 1, '2025-05-01 13:45:26', '2026-05-27 09:32:53'),
|
||||||
|
(483, 24, 'Cappucino', 'Cappucino', 'cappucino', '', '', '', 350.00, 'admin/uploads/products/68137b1927037_1746107161.jpg', 1, '2025-05-01 13:46:01', '2026-05-27 09:16:30'),
|
||||||
|
(484, 24, 'Cafe Latte', 'Latte', 'cafe-latte', '', '', '', 350.00, 'admin/uploads/products/68137b4e7c418_1746107214.jpg', 1, '2025-05-01 13:46:54', '2026-05-27 09:16:03'),
|
||||||
|
(486, 24, 'Mocha', 'Mocha', 'mocha', '', '', '', 360.00, 'admin/uploads/products/68137bc42cb69_1746107332.jpg', 1, '2025-05-01 13:48:52', '2026-05-27 09:33:34'),
|
||||||
|
(489, 24, 'Hot Chocolate', 'Sıcak Çikolata', 'hot-chocolate', '', '', '', 349.98, 'admin/uploads/products/68137c645a87c_1746107492.jpg', 1, '2025-05-01 13:51:32', '2026-05-27 09:33:11'),
|
||||||
|
(490, 24, 'Turkish Coffe', 'Türk Kahvesi', 'turkish-coffe', '', '', '', 250.00, 'admin/uploads/products/68137c9ba36c8_1746107547.jpg', 1, '2025-05-01 13:52:27', '2026-05-27 09:34:09'),
|
||||||
|
(491, 24, 'Turkish Coffe Duble', 'Türk Kahvesi Duble', 'turkish-coffe-duble', '', '', '', 350.00, 'admin/uploads/products/68137cb49cac1_1746107572.jpg', 1, '2025-05-01 13:52:52', '2026-05-27 09:34:01'),
|
||||||
|
(493, 24, 'Filter Coffe', 'Filtre Kahve', 'filter-coffe', '', '', '', 340.00, 'admin/uploads/products/68137d0309280_1746107651.jpg', 1, '2025-05-01 13:54:11', '2026-05-27 09:11:12'),
|
||||||
|
(494, 30, 'Cold Brew', 'Cold Brew', 'cold-brew', '', '', '', 385.00, 'admin/uploads/products/68137d7873a40_1746107768.jpg', 1, '2025-05-01 13:54:37', '2026-05-27 08:50:56'),
|
||||||
|
(495, 30, 'Cold Brew Tonic', 'Cold Brew Tonik', 'cold-brew-tonic', '', '', '', 325.00, 'admin/uploads/products/68137d31d0f8c_1746107697.jpg', 0, '2025-05-01 13:54:57', '2026-05-27 08:49:36'),
|
||||||
|
(496, 30, 'İced Americano', 'İced Americano', 'İced-americano', '', '', '', 350.00, 'admin/uploads/products/68137d5e21a79_1746107742.jpg', 1, '2025-05-01 13:55:42', '2026-05-27 08:53:09'),
|
||||||
|
(497, 30, 'İced Latte', 'İced Latte', 'İced-latte', '', '', '', 380.00, 'admin/uploads/products/68137da678b6f_1746107814.jpg', 1, '2025-05-01 13:56:54', '2026-05-27 08:53:22'),
|
||||||
|
(498, 30, 'İced Caramel Maccihaito', 'İced Caramel Maccihaito', 'İced-caramel-maccihaito', '', '', '', 380.00, 'admin/uploads/products/68137ddc5e47e_1746107868.jpg', 0, '2025-05-01 13:57:48', '2026-05-31 10:00:13'),
|
||||||
|
(499, 30, 'Espresso Tonic', 'Espresso Tonic', 'espresso-tonic', '', '', '', 310.00, 'admin/uploads/products/68137df632c33_1746107894.jpg', 0, '2025-05-01 13:58:14', '2026-05-27 08:52:52'),
|
||||||
|
(500, 30, 'Freddo Espresso', 'Freddo Espresso', 'freddo-espresso', '', '', '', 380.00, 'admin/uploads/products/68137e15be5dc_1746107925.jpg', 1, '2025-05-01 13:58:45', '2026-05-27 08:52:01'),
|
||||||
|
(501, 30, 'Freddo Cappuccino', 'Freddo Cappuccino', 'freddo-cappuccino', '', '', '', 385.00, 'admin/uploads/products/68137e35da26b_1746107957.jpg', 1, '2025-05-01 13:59:17', '2026-05-27 08:51:26'),
|
||||||
|
(502, 30, 'Freddo Flat White', 'Freddo Flat White', 'freddo-flat-white', '', '', '', 385.00, 'admin/uploads/products/68137e760a938_1746108022.jpeg', 1, '2025-05-01 14:00:22', '2026-05-27 08:52:26'),
|
||||||
|
(503, 30, 'Iced Chocolate Mocha', 'Iced Chocolate Mocha', 'iced-chocolate-mocha', '', '', '', 385.00, 'admin/uploads/products/68137ea9c33be_1746108073.jpg', 1, '2025-05-01 14:01:13', '2026-05-27 09:08:44'),
|
||||||
|
(504, 30, 'Iced Chocolate', 'Iced Chocolate', 'iced-chocolate', '', '', '', 385.00, 'admin/uploads/products/68137ef25e266_1746108146.jpg', 1, '2025-05-01 14:02:26', '2026-05-27 09:07:29'),
|
||||||
|
(509, 18, 'Garden Greens Salad', 'Bahçe Yeşillikleri Salatası', 'garden-greens-salad', '', '(Mevsimsel Akdeniz Yeşillikleri(Mediterranean Greens), Ceviz, Çeri domates', '', 800.00, 'admin/uploads/products/6a130a1b0e760_1779632667.png', 1, '2025-05-01 14:12:09', '2026-05-24 14:24:27'),
|
||||||
|
(510, 18, 'Beef Salad', 'Biftekli Salata', 'beef-salad', '', 'Akdeniz Yeşillikleri, Bonfile Parçaları, Çeri domates, Parmesan', '', 1150.00, 'admin/uploads/products/6a13099d80103_1779632541.png', 1, '2025-05-01 14:12:57', '2026-05-24 14:22:21'),
|
||||||
|
(511, 18, 'Chicken Ceasar Salad', 'Sezar Salatası Tavuklu', 'chicken-ceasar-salad', '', '', '', 950.00, 'admin/uploads/products/681381af1a681_1746108847.jpg', 1, '2025-05-01 14:14:07', '2026-05-26 11:16:42'),
|
||||||
|
(512, 18, 'Smoked Salmon Cobb Salad', 'İsli Somonlu Cobb Salata', 'smoked-salmon-cobb-salad', '', 'Akdeniz Yeşillikleri(Mediterranean Greens), Edamame, Kırmızı Soğan Turşusu(Red Onion), Domates(Tomatoes), Bebek Mısır(Baby Corn)', '(Baby Corn, Edamame, Feta Cheese, Red Onion, Tomatoes)', 950.00, 'admin/uploads/products/683ee76652577_1748952934.jpeg', 0, '2025-05-01 14:14:51', '2026-05-24 14:14:18'),
|
||||||
|
(513, 26, 'Grass', 'Grass', 'grass', '', 'Kuzukulağı(Sorrel), Yeşil erik(Green Plum), Turunç(Bitter Orange), Limonotu(Lemongrass)', '', 750.00, 'admin/uploads/products/6818b39749a67_1746449303.jpg', 1, '2025-05-05 12:48:23', '2026-05-27 08:31:00'),
|
||||||
|
(514, 26, 'Placebo', 'Placebo', 'placebo', '', 'Fesleğen(Basil), Salatalık(Cucumber), Zencefil(Ginger)', '', 750.00, 'admin/uploads/products/6818b4a0e6052_1746449568.png', 1, '2025-05-05 12:52:48', '2026-05-27 08:29:38'),
|
||||||
|
(515, 26, 'Whiskey Sour Satsuma', 'Whiskey Sour Satsuma', 'whiskey-sour-satsuma', '', 'Bodrum mandalina(Bodrum Mandalin), Turunç(Bitter Lemon)', '', 750.00, 'admin/uploads/products/6818b4de330c4_1746449630.jpg', 1, '2025-05-05 12:53:50', '2026-05-27 08:29:28'),
|
||||||
|
(516, 26, 'Chili Mango', 'Chili Mango', 'chili-mango', '', 'Mango(Mango), Arnavut biberi(Cayenne Pepper), Turunç(Bitter Lemon)', '', 800.00, 'admin/uploads/products/6818b530c0a50_1746449712.jpg', 1, '2025-05-05 12:55:12', '2026-05-27 08:30:42'),
|
||||||
|
(517, 26, 'Tuxedo', 'Tuxedo', 'tuxedo', '', 'Bergamot(Bergamot), Şeftali(Peach), Vanilya(Vanilla)', '', 749.99, 'admin/uploads/products/6818b692d67ec_1746450066.jpg', 1, '2025-05-05 13:01:06', '2026-05-27 08:29:56'),
|
||||||
|
(518, 26, 'Purple Basil', 'Purple Basil', 'purple-basil', '', 'Reyhan(Sweet Basil), Limon(Lemon)', '', 750.00, 'admin/uploads/products/6818b7b0c2652_1746450352.jpg', 1, '2025-05-05 13:05:52', '2026-05-27 08:29:03'),
|
||||||
|
(520, 26, 'Peach Spirits', 'Peach Spirits', 'peach-spirits', '', 'Red Bull Peach Edition, Aperol ', '', 750.00, 'admin/uploads/products/6a070d113a98a_1778846993.jpeg', 1, '2025-05-05 13:11:19', '2026-05-15 12:10:40'),
|
||||||
|
(521, 27, 'Espresso Martini', 'Espresso Martini', 'espresso-martini', '', 'Kahlua, Vodka, Espresso', '', 800.00, 'admin/uploads/products/6818b9d4816fa_1746450900.jpg', 1, '2025-05-05 13:15:00', '2026-05-27 08:26:38'),
|
||||||
|
(522, 27, 'Long Island', 'Long Island', 'long-island', '', 'Vodka, Gin, Tequila, Triple Sec, Rom, Cola, Lemon(Limon)', '', 1050.00, 'admin/uploads/products/6818b9fc6f4b4_1746450940.jpg', 1, '2025-05-05 13:15:40', '2026-05-27 08:28:22'),
|
||||||
|
(523, 27, 'Margarita', 'Margarita', 'margarita', '', 'Tekila, Triple Sec, Lemon(Limon)', '', 750.00, 'admin/uploads/products/6818ba19ccef1_1746450969.jpg', 1, '2025-05-05 13:16:09', '2026-05-27 08:28:42'),
|
||||||
|
(524, 27, 'Aperol', 'Aperol', 'aperol', '', 'Aperol, Prosecco, Club Soda', '', 880.00, 'admin/uploads/products/6818ba3cc9885_1746451004.jpg', 1, '2025-05-05 13:16:44', '2026-05-27 08:26:20'),
|
||||||
|
(525, 25, 'Ayran', 'Ayran', 'ayran', '', '', '', 175.00, 'admin/uploads/products/6818bb0b950d3_1746451211.jpg', 1, '2025-05-05 13:20:11', '2026-05-27 08:25:53'),
|
||||||
|
(526, 25, 'Capri-Sun', 'Capri-Sun', 'capri-sun', '', '', '', 185.00, 'admin/uploads/products/6818bb30539ed_1746451248.jpg', 1, '2025-05-05 13:20:48', '2026-05-27 08:24:04'),
|
||||||
|
(527, 25, 'Coca Cola', 'Coca Cola', 'coca-cola', '', '', '', 240.00, 'admin/uploads/products/6818bb4997a3c_1746451273.jpg', 1, '2025-05-05 13:21:13', '2026-05-27 08:25:31'),
|
||||||
|
(528, 25, 'Churchill', 'Churchill', 'churchill', '', '', '', 440.00, 'admin/uploads/products/6818bb7a8067d_1746451322.jpg', 1, '2025-05-05 13:22:02', '2026-05-27 08:25:19'),
|
||||||
|
(529, 25, 'Fanta', 'Fanta', 'fanta', '', '', '', 240.00, 'admin/uploads/products/6818bb9a352ed_1746451354.jpg', 1, '2025-05-05 13:22:34', '2026-05-27 08:23:55'),
|
||||||
|
(530, 25, 'Sprite', 'Sprite', 'sprite', '', '', '', 240.00, 'admin/uploads/products/68209088e5a52_1746964616.png', 1, '2025-05-05 13:23:00', '2026-05-27 08:23:46'),
|
||||||
|
(531, 25, 'Uludağ Premium Soda', 'Uludağ Premium Soda', 'uludağ-premium-soda', '', '', '', 220.00, 'admin/uploads/products/6818bbd74b39a_1746451415.jpg', 1, '2025-05-05 13:23:35', '2026-05-27 08:23:05'),
|
||||||
|
(533, 25, 'Water (Uludag Glass Bottle)', 'Su (Uludağ Cam Şişe)', 'water-(uludag-glass-bottle)', '', '', '', 100.00, 'admin/uploads/products/68208fd451d73_1746964436.png', 1, '2025-05-05 13:24:46', '2026-05-27 08:23:23'),
|
||||||
|
(534, 28, 'Becks 33cl', 'Becks 33cl', 'becks-33cl', '', '', '', 440.00, 'admin/uploads/products/6818bc562bb45_1746451542.png', 1, '2025-05-05 13:25:42', '2026-05-27 08:21:53'),
|
||||||
|
(535, 28, 'Bud 33cl', 'Bud 33cl', 'bud-33cl', '', '', '', 440.00, 'admin/uploads/products/6818bc90beca4_1746451600.jpg', 1, '2025-05-05 13:26:40', '2026-05-27 08:21:35'),
|
||||||
|
(536, 28, 'Belfast 50cl', 'Belfast 50cl', 'belfast-50cl', '', '', '', 440.00, 'admin/uploads/products/6818bcb04691e_1746451632.jpg', 1, '2025-05-05 13:27:12', '2026-05-27 08:19:57'),
|
||||||
|
(537, 28, 'Bomonti Filtresiz 50cl', 'Bomonti Filtresiz 50cl', 'bomonti-filtresiz-50cl', '', '', '', 440.00, 'admin/uploads/products/6818bcd16e5fa_1746451665.jpg', 1, '2025-05-05 13:27:45', '2026-05-27 08:19:32'),
|
||||||
|
(538, 28, 'Corona 35.5cl', 'Corona 35.5cl', 'corona-35.5cl', '', '', '', 470.00, 'admin/uploads/products/6818bcea5e878_1746451690.png', 1, '2025-05-05 13:28:10', '2026-05-27 08:19:09'),
|
||||||
|
(539, 28, 'Efes 50cl', 'Efes 50cl', 'efes-50cl', '', '', '', 440.00, 'admin/uploads/products/6818bcff46af4_1746451711.jpg', 1, '2025-05-05 13:28:31', '2026-05-27 08:18:51'),
|
||||||
|
(540, 28, 'Efes Malt 50cl', 'Efes Malt 50cl', 'efes-malt-50cl', '', '', '', 440.00, 'admin/uploads/products/6818bd18b70da_1746451736.jpg', 1, '2025-05-05 13:28:56', '2026-05-27 08:18:41'),
|
||||||
|
(541, 28, 'Efes Green 50cl', 'Efes Green 50cl', 'efes-green-50cl', '', '', '', 440.00, 'admin/uploads/products/6818bd2dc0d4b_1746451757.jpg', 1, '2025-05-05 13:29:17', '2026-05-27 08:17:31'),
|
||||||
|
(542, 28, 'Erdinger 33cl', 'Erdinger 33cl', 'erdinger-33cl', '', '', '', 470.00, 'admin/uploads/products/6818bd4172092_1746451777.jpg', 1, '2025-05-05 13:29:37', '2026-05-27 08:18:22'),
|
||||||
|
(544, 28, 'Heineken 33cl', 'Heineken 33cl', 'heineken-33cl', '', '', '', 470.00, 'admin/uploads/products/6818bd6639460_1746451814.jpg', 1, '2025-05-05 13:30:14', '2026-05-27 08:16:56'),
|
||||||
|
(545, 28, 'Hoegaarden 33cl', 'Hoegaarden 33cl', 'hoegaarden-33cl', '', '', '', 370.00, 'admin/uploads/products/6818bd7a9c0df_1746451834.jpg', 0, '2025-05-05 13:30:34', '2025-09-13 11:19:02'),
|
||||||
|
(546, 28, 'Leffe Blonde 33cl', 'Leffe Blonde 33cl', 'leffe-blonde-33cl', '', '', '', 370.00, 'admin/uploads/products/6818bd91b8a91_1746451857.jpg', 0, '2025-05-05 13:30:57', '2025-09-13 11:18:48'),
|
||||||
|
(547, 28, 'Miller 33cl', 'Miller 33cl', 'miller-33cl', '', '', '', 440.00, 'admin/uploads/products/6818bdadeb092_1746451885.jpg', 1, '2025-05-05 13:31:25', '2026-05-27 08:16:14'),
|
||||||
|
(549, 18, 'Baby Lettuce Avocado Salmon Salad', 'Yedikule Avokado Somon Salatası', 'baby-lettuce-avocado-salmon-salad', '', 'Marul(Baby Lettuce), Avokado(Avocado), Parmesan(Parmesan) Grilled Salmon', '(Yedikule Mini Marul, Avokado, Izgara Somon, Parmesan)', 1200.00, 'admin/uploads/products/6a1308f419102_1779632372.png', 0, '2025-05-07 17:42:25', '2026-05-26 12:29:07'),
|
||||||
|
(551, 34, 'Deep Fried Breaded Chicken', 'Panelenmiş Tavuk Parçaları', 'deep-fried-breaded-chicken', '', '(Çıtır Patates, Acı-Tatlı Sos)(French Fries, Sweet Chilli Sauce)', '', 950.00, 'admin/uploads/products/68ab0fa0ea103_1756041120.jpg', 1, '2025-05-07 18:05:18', '2026-06-04 08:20:59'),
|
||||||
|
(552, 17, 'Frankfurter Grilled', 'Izgara Frankfurter Sosis', 'frankfurter-grilled', '', '(Patates Kızartması, Hardallı Mayonez, Pancar Püresi)(Steak Fries, Mustard Sauce)', '', 700.00, 'admin/uploads/products/6847d9d85c1a4_1749539288.jpeg', 0, '2025-05-07 18:10:23', '2026-05-24 14:06:22'),
|
||||||
|
(553, 16, 'Taglia Di Manzo', 'Taglia Di Manzo', 'taglia-di-manzo', '', '(Izgara Bonfile Dilimleri, Roka, Balzemik Sirke, Çeri Domates, Parmesan)(Rocket, Balsamic Vinegar, Parmesan Cheese)', '', 1250.00, 'admin/uploads/products/683efa2e12555_1748957742.jpeg', 1, '2025-05-07 18:26:58', '2026-05-26 12:27:28'),
|
||||||
|
(571, 13, 'Scrambled Eggs with Sautéed Beef', 'Kavurmalı Yumurta', 'scrambled-eggs-with-sautéed-beef', '', '', '', 700.00, 'admin/uploads/products/68208d7225672_1746963826.jpg', 0, '2025-05-11 11:43:46', '2026-05-26 12:14:34'),
|
||||||
|
(572, 25, 'Ice Tea (Lemon, Peach, Mango)', 'Ice Tea (Limon,Şeftali,Mango)', 'ice-tea-(lemon,-peach,-mango)', '', '', '', 240.00, 'admin/uploads/products/682090edb6dfa_1746964717.jpg', 1, '2025-05-11 11:58:37', '2026-05-27 08:39:14'),
|
||||||
|
(5551, 2, 'Penne Arabiata Gluten Free', 'Glütensiz Penne', 'penne-arabiata-gluten-free', '', '', '', 900.00, 'admin/uploads/products/682c57cbb813b_1747736523.jpg', 0, '2025-05-20 10:22:03', '2026-05-26 12:15:32'),
|
||||||
|
(5552, 2, 'Gluten Free Fusulli', 'Glütensiz Fusulli', 'gluten-free-fusulli', '', '', '', 900.00, 'admin/uploads/products/682c57fb865e8_1747736571.jpg', 0, '2025-05-20 10:22:51', '2026-05-26 12:15:58'),
|
||||||
|
(5553, 31, 'Casamigos Shot', 'Casamigos Shot', 'casamigos-shot', '', '', '', 350.00, 'admin/uploads/products/683c221dc08c7_1748771357.jpg', 1, '2025-06-01 09:37:02', '2026-05-27 08:34:51'),
|
||||||
|
(5554, 31, 'Viski Shot', 'Viski Shot', 'viski-shot', '', '', '', 350.00, 'admin/uploads/products/683c202cef39a_1748770860.jpg', 1, '2025-06-01 09:41:00', '2026-05-27 08:33:23'),
|
||||||
|
(5555, 31, 'Jagermeister Shot', 'Jagermeister Shot', 'jagermeister-shot', '', '', '', 350.00, 'admin/uploads/products/683c20a7bc05b_1748770983.jpg', 1, '2025-06-01 09:43:03', '2026-05-27 08:33:08'),
|
||||||
|
(5556, 31, 'Baileys', 'Baileys', 'baileys', '', '', '', 350.00, 'admin/uploads/products/683c21120411c_1748771090.jpg', 1, '2025-06-01 09:44:50', '2026-05-27 08:32:01'),
|
||||||
|
(5557, 31, 'Smirnoff Shot', 'Smirnoff Shot', 'smirnoff-shot', '', '', '', 350.00, 'admin/uploads/products/683c214ad535b_1748771146.png', 1, '2025-06-01 09:45:46', '2026-05-27 08:31:47'),
|
||||||
|
(5567, 25, 'Red Bull Energy Drink', 'Red Bull Energy Drink', 'red-bull-energy-drink', '', '', '', 260.00, 'admin/uploads/products/683dae23058a7_1748872739.jpg', 1, '2025-06-02 13:51:00', '2026-05-15 12:00:50'),
|
||||||
|
(5568, 25, 'Red Bull SugarFree', 'Red Bull Şekersiz', 'red-bull-sugarfree', '', '', '', 260.00, 'admin/uploads/products/683dae084231e_1748872712.jpeg', 1, '2025-06-02 13:51:55', '2026-05-15 12:00:34'),
|
||||||
|
(5570, 25, 'Red Bull ZERO', 'Red Bull ZERO', 'red-bull-zero', '', '', '', 260.00, 'admin/uploads/products/6a070b45a26b6_1778846533.jpeg', 1, '2025-06-02 13:53:27', '2026-05-15 12:02:13'),
|
||||||
|
(5571, 25, 'Red Bull White Edition', 'Red Bull White Edition', 'red-bull-white-edition', '', '', '', 260.00, 'admin/uploads/products/683dadcb818a6_1748872651.jpeg', 1, '2025-06-02 13:54:11', '2026-05-15 11:59:58'),
|
||||||
|
(5572, 25, 'Red Bull Blue Edition', 'Red Bull Blue Edition', 'red-bull-blue-edition', '', '', '', 260.00, 'admin/uploads/products/683dad951d7ca_1748872597.jpeg', 1, '2025-06-02 13:56:37', '2026-05-15 11:59:38'),
|
||||||
|
(5604, 10, 'Smoked Salmon Pizza', 'Füme Somonlu Pizza', 'smoked-salmon-pizza', 'Smoked salmon, red onion, capers, chives', '', 'Füme somon, kırmızı soğan, kapari, Frenk soğanı.', 950.00, 'admin/uploads/products/686902ff5cb5d_1751712511.jpg', 0, '2025-07-05 10:48:31', '2026-05-24 13:44:55'),
|
||||||
|
(5605, 28, 'efes 33cl', 'efes 33cl', 'efes-33cl', '', '', '', 340.00, '', 0, '2025-07-24 10:11:46', '2026-05-27 07:36:38'),
|
||||||
|
(5606, 32, 'Smirnoff Vodka', 'Smirnoff Vodka', 'smirnoff-vodka', '', '', '', 750.00, '', 1, '2025-07-29 10:50:15', '2026-05-27 09:56:11'),
|
||||||
|
(5607, 32, 'Ketel One Vodka', 'Ketel One Vodka', 'ketel-one-vodka', '', '', '', 1090.00, '', 1, '2025-07-29 10:50:48', '2026-05-27 09:56:43'),
|
||||||
|
(5608, 32, 'Don Julio', 'Don Julio', 'don-julio', '', '', '', 1100.00, '', 1, '2025-07-29 10:52:46', '2026-05-27 09:55:32'),
|
||||||
|
(5609, 32, 'Casamigos Tequila', 'Casamigos Tequila', 'casamigos-tequila', '', '', '', 850.00, '', 1, '2025-07-29 10:53:13', '2026-05-27 09:55:23'),
|
||||||
|
(5610, 32, 'Lagavulin', 'Lagavulin', 'lagavulin', '', '', '', 1300.00, '', 1, '2025-07-29 10:58:04', '2026-05-27 09:56:55'),
|
||||||
|
(5611, 32, 'Talisker', 'Talisker', 'talisker', '', '', '', 1190.00, '', 1, '2025-07-29 10:58:14', '2026-05-27 09:55:05'),
|
||||||
|
(5612, 32, 'Singleton', 'Singleton', 'singleton', '', '', '', 879.98, '', 1, '2025-07-29 10:58:30', '2026-05-27 09:54:54'),
|
||||||
|
(5613, 32, 'Dimple', 'Dimple', 'dimple', '', '', '', 790.00, '', 1, '2025-07-29 10:59:46', '2026-05-27 09:54:46'),
|
||||||
|
(5614, 32, 'Black Label', 'Black Label', 'black-label', '', '', '', 810.00, '', 1, '2025-07-29 11:00:14', '2026-05-27 09:54:38'),
|
||||||
|
(5615, 32, 'Captain Morgan Rom', 'Captain Morgan Rom', 'captain-morgan-rom', '', '', '', 750.00, '', 1, '2025-07-29 11:01:11', '2026-05-27 09:54:27'),
|
||||||
|
(5616, 32, 'Gordon\'s Gin', 'Gordon\'s Gin', 'gordon\'s-gin', '', '', '', 749.99, '', 1, '2025-07-29 11:01:49', '2026-05-27 09:37:49'),
|
||||||
|
(5617, 32, 'Gordon\'s Pink', 'Gordon\'s Pink', 'gordon\'s-pink', '', '', '', 750.00, '', 1, '2025-07-29 11:02:01', '2026-05-27 09:37:38'),
|
||||||
|
(5618, 32, 'Gordon\'s Sicilian Lemon', 'Gordon\'s Sicilian Lemon', 'gordon\'s-sicilian-lemon', '', '', '', 749.99, '', 1, '2025-07-29 11:02:38', '2026-05-27 09:37:28'),
|
||||||
|
(5619, 32, 'Tanqueray No.Ten', 'Tanqueray No.Ten', 'tanqueray-no.ten', '', '', '', 1189.99, '', 1, '2025-07-29 11:04:03', '2026-05-27 09:37:17'),
|
||||||
|
(5620, 32, 'Tanqueray Gin', 'Tanqueray Gin', 'tanqueray-gin', '', '', '', 1060.00, '', 1, '2025-07-29 11:04:23', '2026-05-27 08:35:24'),
|
||||||
|
(5621, 25, 'Red Bull Peach Edition', 'Red Bull Peach Edition', 'red-bull-peach-edition', '', '', '', 260.00, 'admin/uploads/products/6a070bf9396ff_1778846713.jpeg', 1, '2026-05-15 12:05:13', '2026-05-15 12:06:51'),
|
||||||
|
(5622, 17, 'Panini Sandwich', 'Panini Sandwich', 'panini-sandwich', '', '(Manda Mozzarella Peyniri, Avokado, Pesto Sos, Patates Kızartması)(Avocado, Buffalo Mozzarella, Pesto Sauce)\r\n', '(Manda Mozzarella Peyniri, Avokado, Pesto Sos, Patates Kızartması)(Avocado, Buffalo Mozzarella, Pesto Sauce)\r\n', 950.00, 'admin/uploads/products/6a1eccf6a7637_1780403446.jpeg', 1, '2026-05-29 08:24:21', '2026-06-02 12:54:09'),
|
||||||
|
(5623, 17, 'Sliced Dry Beef Rib Sandwich', 'Biftek Sandviç', 'sliced-dry-beef-rib-sandwich', '', '(Izgara Bonfile Dilimleri, İsli Kaburga, Çedar Peyniri, Hardallı Mayonez)(Grilled Tenderloin Sliced, Smoked Rib, Chedar Cheese, Mustard Mayo)', '(Izgara Bonfile Dilimleri, İsli Kaburga, Çedar Peyniri, Hardallı Mayonez)(Grilled Tenderloin Sliced, Smoked Rib, Chedar Cheese, Mustard Mayo)', 1150.00, 'admin/uploads/products/6a1ecf25c21b4_1780404005.png', 1, '2026-05-29 08:25:51', '2026-06-02 12:40:05'),
|
||||||
|
(5624, 17, 'Burrito', 'Burrito', 'burrito', 'Sautéed chicken strips with vegetables, kidney beans, cheddar cheese, and BBQ sauce, wrapped in a tortilla.', 'Barbekü soslu, cheddarlı, sebzeli,meksika fasulyeli tavuk parçaları, tortilla ekmeğine sarılmış.', '', 1050.00, 'admin/uploads/products/6a1ecea737f48_1780403879.png', 1, '2026-06-02 12:37:59', '2026-06-02 12:37:59'),
|
||||||
|
(5625, 14, 'Omelette Vegetables/Cheese', 'Omlet Sebzeli/Peynirli', 'omelette-vegetables/cheese', '', '', '', 550.00, 'admin/uploads/products/6a1ed1bbb1053_1780404667.png', 1, '2026-06-02 12:51:07', '2026-06-02 12:51:07'),
|
||||||
|
(5626, 16, 'Grilled Salmon', 'Izgara Somon', 'grilled-salmon', '', '', 'Izgara somon, küp patatesler, narenciye soslu yeşil salata', 1200.00, 'admin/uploads/products/6a2205943a755_1780614548.jpg', 0, '2026-06-04 23:09:08', '2026-06-04 23:09:16'),
|
||||||
|
(5627, 18, 'Salmon Salade', 'Izgara Somon Salatası', 'salmon-salade', '', '', 'Izgara somon parçaları, Akdeniz yeşillikleri, badem, kırmızı soğan, avokado, çeri domates, tarator sos', 1100.00, 'admin/uploads/products/6a220779e4e5d_1780615033.jpg', 0, '2026-06-04 23:17:13', '2026-06-04 23:17:23'),
|
||||||
|
(5628, 14, 'London Breakfast Plate', 'Londra\'dan kahvaltı', 'london-breakfast-plate', '', '', 'Dana füme sosis,göz yumurta, yeşil salata, çeri, Meksika fasulyesi, feta peynir, ekşi maya ekmek', 1100.00, 'admin/uploads/products/6a220a98a0ef5_1780615832.jpg', 0, '2026-06-04 23:30:32', '2026-06-04 23:30:41');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için tablo yapısı `settings`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `settings` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`site_title` varchar(255) DEFAULT NULL,
|
||||||
|
`site_description` text DEFAULT NULL,
|
||||||
|
`logo_url` varchar(255) DEFAULT NULL,
|
||||||
|
`header_text` varchar(255) DEFAULT NULL,
|
||||||
|
`phone_number` varchar(20) DEFAULT NULL,
|
||||||
|
`wifi_name` varchar(255) DEFAULT NULL,
|
||||||
|
`wifi_password` varchar(255) DEFAULT NULL,
|
||||||
|
`instagram_url` varchar(255) DEFAULT NULL,
|
||||||
|
`facebook_url` varchar(255) DEFAULT NULL,
|
||||||
|
`twitter_url` varchar(255) DEFAULT NULL,
|
||||||
|
`youtube_url` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`address` text DEFAULT NULL,
|
||||||
|
`working_hours_weekday` varchar(50) DEFAULT '09:00 - 22:00',
|
||||||
|
`working_hours_saturday` varchar(50) DEFAULT '10:00 - 22:00',
|
||||||
|
`working_hours_sunday` varchar(50) DEFAULT '10:00 - 21:00'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_turkish_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo döküm verisi `settings`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `settings` (`id`, `site_title`, `site_description`, `logo_url`, `header_text`, `phone_number`, `wifi_name`, `wifi_password`, `instagram_url`, `facebook_url`, `twitter_url`, `youtube_url`, `created_at`, `updated_at`, `address`, `working_hours_weekday`, `working_hours_saturday`, `working_hours_sunday`) VALUES
|
||||||
|
(1, 'Kite Beach Akyaka', 'Kite Beach Akyaka Menü', 'admin/uploads/logo_1747056472.png', '', '+90 533 081 61 81', 'test', 'test', 'https://www.instagram.com/kitebeachakyaka', '', '', '', '2024-12-30 18:47:43', '2025-05-12 13:28:31', 'Akçapınar, 48640 Ula/Muğla', '09:00 - 22:00', '10:00 - 22:00', 'KAPALI');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için tablo yapısı `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`username` varchar(50) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`email` varchar(100) NOT NULL,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`last_login` datetime DEFAULT NULL,
|
||||||
|
`status` tinyint(1) DEFAULT 1,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_turkish_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo döküm verisi `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `users` (`id`, `username`, `password`, `email`, `name`, `last_login`, `status`, `created_at`) VALUES
|
||||||
|
(2, 'moyda', '$2y$10$LvjMn.fMTngSvTAKf60esuCBKCydWW3xMOu8.BpkAeAwCCZPCo99i', 'moydabeach@gmail.com', 'Admin User', '2026-06-05 01:54:25', 1, '2025-03-14 15:40:01'),
|
||||||
|
(6, 'moygroup', '$2y$10$LrAaYei6/wjQpLaMgmm2lOuqk9yxr4.Z/B3ATqFd8OT4/tTyozANi', 'moydabeach1@gmail.com', 'Administrator', '2026-04-29 14:54:26', 1, '2025-03-14 15:40:01');
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dökümü yapılmış tablolar için indeksler
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için indeksler `banners`
|
||||||
|
--
|
||||||
|
ALTER TABLE `banners`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için indeksler `categories`
|
||||||
|
--
|
||||||
|
ALTER TABLE `categories`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için indeksler `products`
|
||||||
|
--
|
||||||
|
ALTER TABLE `products`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `category_id` (`category_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için indeksler `settings`
|
||||||
|
--
|
||||||
|
ALTER TABLE `settings`
|
||||||
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için indeksler `users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `username` (`username`),
|
||||||
|
ADD UNIQUE KEY `email` (`email`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dökümü yapılmış tablolar için AUTO_INCREMENT değeri
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için AUTO_INCREMENT değeri `banners`
|
||||||
|
--
|
||||||
|
ALTER TABLE `banners`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için AUTO_INCREMENT değeri `categories`
|
||||||
|
--
|
||||||
|
ALTER TABLE `categories`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=35;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için AUTO_INCREMENT değeri `products`
|
||||||
|
--
|
||||||
|
ALTER TABLE `products`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5629;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için AUTO_INCREMENT değeri `settings`
|
||||||
|
--
|
||||||
|
ALTER TABLE `settings`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Tablo için AUTO_INCREMENT değeri `users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||