first commit
@@ -0,0 +1,9 @@
|
|||||||
|
.next
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
.git
|
||||||
|
.vscode
|
||||||
|
docs
|
||||||
|
README.md
|
||||||
|
AGENTS.md
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Framework: Next.js 16, App Router, TypeScript strict
|
||||||
|
- Styling: Tailwind CSS v4
|
||||||
|
- UI: shadcn/ui (new-york style, OKLCH)
|
||||||
|
- Animation: Framer Motion
|
||||||
|
- Icons: Lucide React
|
||||||
|
- i18n: next-intl
|
||||||
|
- ORM: Prisma + PostgreSQL
|
||||||
|
- Auth: NextAuth.js v5
|
||||||
|
- Media: Cloudinary
|
||||||
|
- Deploy: Coolify (Docker, standalone output)
|
||||||
|
|
||||||
|
## Sabit Tercihler
|
||||||
|
- Mock data: USE_MOCK=true (demo aşaması)
|
||||||
|
- proxy.ts kullan — middleware.ts deprecated (Next.js 15.3+)
|
||||||
|
- İletişim formu sadece /iletisim sayfasında — ana sayfada olmaz
|
||||||
|
- Footer'da "Created by ayris.tech" linki zorunlu
|
||||||
|
- Dockerfile'da dummy DATABASE_URL (prisma generate için)
|
||||||
|
|
||||||
|
## Altyapı
|
||||||
|
- Gitea: https://git.ayris.tech (kullanıcı: ayrisdev)
|
||||||
|
- Coolify: https://client2.ayris.tech
|
||||||
|
- Cloudflare zone: ayris.tech
|
||||||
|
- Server IP: 188.245.175.169
|
||||||
|
|
||||||
|
## docs/ Klasörü
|
||||||
|
- docs/prd.md → ana içerik kaynağı
|
||||||
|
- docs/*.html → varsa mevcut site içeriği
|
||||||
|
- docs/*.md → ek belgeler
|
||||||
|
|
||||||
|
## Aktif Skill'ler
|
||||||
|
- nextjs-seo → sitemap, metadata, robots.txt
|
||||||
|
- next-best-practices → kod kalitesi
|
||||||
|
- nextjs-app-router-patterns → Server Actions, Suspense
|
||||||
|
- demo-site → komple site üretimi
|
||||||
|
- design-demo → görsel kalite
|
||||||
|
- coolify-deploy → deploy pipeline
|
||||||
|
|
||||||
|
## Proje Özel Notlar
|
||||||
|
<!-- Buraya proje bazlı notlar ekle -->
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
# Prisma generate için dummy URL — build sırasında gerçek DB gerekmez
|
||||||
|
ARG DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy
|
||||||
|
ENV DATABASE_URL=$DATABASE_URL
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
RUN mkdir .next && chown nextjs:nodejs .next
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { signOut } from 'next-auth/react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { LayoutDashboard, Users, Settings, LogOut, Menu, X } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
|
||||||
|
{ name: 'Kullanıcılar', href: '/admin/users', icon: Users },
|
||||||
|
{ name: 'Ayarlar', href: '/admin/settings', icon: Settings },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
|
||||||
|
{/* Mobile sidebar backdrop */}
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40 bg-gray-900/80 lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className={`
|
||||||
|
fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-950 border-r border-gray-200 dark:border-gray-800
|
||||||
|
transform transition-transform duration-200 ease-in-out lg:translate-x-0 lg:static lg:inset-0
|
||||||
|
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||||
|
`}>
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="h-16 flex items-center px-6 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<h1 className="text-lg font-bold text-gray-900 dark:text-white">Admin Paneli</h1>
|
||||||
|
<button
|
||||||
|
className="ml-auto lg:hidden text-gray-500"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
|
||||||
|
{navigation.map((item) => {
|
||||||
|
const isActive = pathname.endsWith(item.href)
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={`
|
||||||
|
flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors
|
||||||
|
${isActive
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<item.icon className={`mr-3 flex-shrink-0 h-5 w-5 ${isActive ? 'text-gray-900 dark:text-white' : 'text-gray-400'}`} />
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<button
|
||||||
|
onClick={() => signOut({ callbackUrl: '/' })}
|
||||||
|
className="flex w-full items-center px-3 py-2.5 text-sm font-medium text-red-600 dark:text-red-400 rounded-md hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="mr-3 h-5 w-5" />
|
||||||
|
Çıkış Yap
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
|
<header className="h-16 flex items-center lg:hidden bg-white dark:bg-gray-950 border-b border-gray-200 dark:border-gray-800 px-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
className="text-gray-500 hover:text-gray-900 dark:hover:text-white focus:outline-none"
|
||||||
|
>
|
||||||
|
<Menu className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
<span className="ml-4 text-lg font-bold text-gray-900 dark:text-white">Admin Paneli</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { auth } from '@/lib/auth'
|
||||||
|
|
||||||
|
export default async function AdminDashboardPage() {
|
||||||
|
const session = await auth()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Dashboard</h2>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mt-2">
|
||||||
|
Hoş geldiniz, {session?.user?.name || session?.user?.email}. İşte projenizin genel görünümü.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{/* Placeholder Stat Cards */}
|
||||||
|
{[
|
||||||
|
{ name: 'Toplam Kullanıcı', stat: '1,245' },
|
||||||
|
{ name: 'Aktif Oturumlar', stat: '42' },
|
||||||
|
{ name: 'Yeni Kayıtlar', stat: '8' },
|
||||||
|
{ name: 'Sistem Durumu', stat: 'Online' },
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.name}
|
||||||
|
className="overflow-hidden rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 px-4 py-5 shadow-sm sm:p-6"
|
||||||
|
>
|
||||||
|
<dt className="truncate text-sm font-medium text-gray-500 dark:text-gray-400">{item.name}</dt>
|
||||||
|
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
||||||
|
{item.stat}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm">
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Son Aktiviteler</h3>
|
||||||
|
<div className="mt-4 border-t border-gray-100 dark:border-gray-800">
|
||||||
|
<div className="py-4 text-sm text-gray-500">
|
||||||
|
Henüz aktivite kaydı bulunmuyor.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Archivo } from "next/font/google";
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getMessages, setRequestLocale } from 'next-intl/server';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { routing } from '@/i18n/routing';
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
const archivo = Archivo({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-archivo",
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
default: "AyrisLegal — Hukuki Düşünce Ortağınız",
|
||||||
|
template: "%s | AyrisLegal",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"AyrisLegal, solo ve küçük büro avukatları için yapay zeka destekli hukuki asistanıdır. Dosya analizi, çok turlu hukuki sohbet ve emsal karar arama tek bir platformda.",
|
||||||
|
keywords: ["avukat yazılımı", "hukuki yapay zeka", "emsal karar arama", "hukuk ofisi yönetim", "AyrisLegal"],
|
||||||
|
authors: [{ name: "ayris.tech", url: "https://ayris.tech" }],
|
||||||
|
creator: "ayris.tech",
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "tr_TR",
|
||||||
|
siteName: "AyrisLegal",
|
||||||
|
title: "AyrisLegal — Hukuki Düşünce Ortağınız",
|
||||||
|
description:
|
||||||
|
"Pasif araştırma değil, aktif düşünce ortağı. Avukatlar için yapay zeka destekli hukuki asistan.",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "AyrisLegal — Hukuki Düşünce Ortağınız",
|
||||||
|
description:
|
||||||
|
"Pasif araştırma değil, aktif düşünce ortağı. Avukatlar için yapay zeka destekli hukuki asistan.",
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return routing.locales.map((locale) => ({locale}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}>) {
|
||||||
|
const { locale } = await params;
|
||||||
|
|
||||||
|
if (!routing.locales.includes(locale as any)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestLocale(locale);
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang={locale}
|
||||||
|
className={`${archivo.variable} h-full antialiased`}
|
||||||
|
>
|
||||||
|
<body className="min-h-full flex flex-col" suppressHydrationWarning>
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
{children}
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { signIn } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
const result = await signIn('credentials', {
|
||||||
|
redirect: false,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setError('Geçersiz e-posta veya şifre')
|
||||||
|
setLoading(false)
|
||||||
|
} else {
|
||||||
|
router.push('/admin')
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
|
||||||
|
<div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-100 dark:border-gray-800 overflow-hidden">
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Admin Girişi</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">Yönetim paneline erişmek için giriş yapın</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-md text-sm mb-6 border border-red-100">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="email">
|
||||||
|
E-posta
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors"
|
||||||
|
placeholder="admin@ayris.tech"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="password">
|
||||||
|
Şifre
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-md transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-xs text-gray-400">
|
||||||
|
Demo credentials: admin@ayris.tech / admin
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { motion, useInView } from 'framer-motion'
|
||||||
|
import ContactForm from '@/components/contact-form'
|
||||||
|
|
||||||
|
/* ─── Design system tokens (Classical) ────────────────────────────────── */
|
||||||
|
const T = {
|
||||||
|
bg: '#f3f2f2',
|
||||||
|
surface: '#eae9e9',
|
||||||
|
text: '#201f1d',
|
||||||
|
divider: 'rgba(32,31,29,0.16)',
|
||||||
|
accent: '#b68235',
|
||||||
|
accentDark: '#7d5411',
|
||||||
|
neutral100: '#f8f4f4',
|
||||||
|
fontHead: '"Sora", sans-serif',
|
||||||
|
fontBody: '"Inter", sans-serif',
|
||||||
|
radiusMd: '4px',
|
||||||
|
radiusLg: '7px',
|
||||||
|
shadowSm: '0 1px 2px rgba(45,43,43,0.14)',
|
||||||
|
shadowMd: '0 3px 10px rgba(45,43,43,0.16)',
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Reveal animation ─────────────────────────────────────────────────── */
|
||||||
|
function Reveal({ children, className = '', delay = 0 }: {
|
||||||
|
children: React.ReactNode; className?: string; delay?: number
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const inView = useInView(ref, { once: true, margin: '-80px' })
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 28 }}
|
||||||
|
transition={{ duration: 0.7, ease: [0.25, 0.46, 0.45, 0.94], delay }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Rail nav ──────────────────────────────────────────────────────────── */
|
||||||
|
const RAIL_ITEMS = [
|
||||||
|
{ id: '02', href: '#is-akisi', label: '02' },
|
||||||
|
{ id: '03', href: '#farklilasim', label: '03' },
|
||||||
|
{ id: '04', href: '#ozellikler', label: '04' },
|
||||||
|
{ id: '05', href: '#fiyatlandirma', label: '05' },
|
||||||
|
{ id: '06', href: '#guven', label: '06' },
|
||||||
|
{ id: '07', href: '#iletisim', label: '07' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ─── Component ─────────────────────────────────────────────────────────── */
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<div style={{ background: T.bg, color: T.text, fontFamily: T.fontBody, minHeight: '100vh', position: 'relative' }}>
|
||||||
|
|
||||||
|
{/* ── Inline fonts ── */}
|
||||||
|
<style>{`
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Sora:wght@300;400;500;600&display=swap');
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; }
|
||||||
|
a { color: ${T.accent}; text-underline-offset: 3px; }
|
||||||
|
:focus-visible { outline: 2px solid ${T.accent}; outline-offset: 2px; }
|
||||||
|
::selection { background: rgba(182,130,53,0.25); }
|
||||||
|
.rail-link { text-decoration: none; writing-mode: vertical-rl; font-size: 11px; letter-spacing: 0.08em; transition: color .2s; font-family: ${T.fontHead}; }
|
||||||
|
.hr-line { height: 1px; border: 0; background: ${T.divider}; margin: 0; }
|
||||||
|
.plate { filter: sepia(0.22) saturate(0.82) contrast(1.05); box-sizing: border-box; border: 6px solid ${T.surface}; outline: 1px solid ${T.divider}; }
|
||||||
|
.btn-primary { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; text-decoration: none; font-family: ${T.fontHead}; font-weight: 600; font-size: 14px; color: ${T.accent}; background: transparent; border: 1px solid ${T.accent}; padding: 9px 16px; border-radius: ${T.radiusMd}; transition: background .15s; }
|
||||||
|
.btn-primary:hover { background: rgba(182,130,53,0.1); }
|
||||||
|
.btn-ghost { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; text-decoration: none; font-family: ${T.fontHead}; font-weight: 600; font-size: 14px; color: ${T.accent}; background: transparent; border: none; padding: 9px 4px; border-radius: ${T.radiusMd}; transition: background .15s; }
|
||||||
|
.btn-ghost:hover { background: rgba(182,130,53,0.08); }
|
||||||
|
.btn-primary-lg { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; text-decoration: none; font-family: ${T.fontHead}; font-weight: 600; font-size: 15px; color: ${T.accent}; background: transparent; border: 1px solid ${T.accent}; padding: 13px 24px; border-radius: ${T.radiusMd}; transition: background .15s; width: 100%; }
|
||||||
|
.btn-primary-lg:hover { background: rgba(182,130,53,0.1); }
|
||||||
|
.card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
||||||
|
.card { display: flex; flex-direction: column; gap: 9px; padding: 13.8px; border-radius: ${T.radiusMd}; background: transparent; border: 1px solid ${T.divider}; box-shadow: ${T.shadowSm}; }
|
||||||
|
.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: ${T.accent}; }
|
||||||
|
.card-title { font-family: ${T.fontHead}; font-weight: 600; font-size: 17px; line-height: 1.2; margin: 0; }
|
||||||
|
.card-body { margin: 0; font-size: 13px; opacity: 0.8; }
|
||||||
|
.field > label { display: block; font-size: 12px; margin-bottom: 5px; color: rgba(32,31,29,0.7); }
|
||||||
|
.legal-input { width: 100%; min-height: 36px; padding: 6px 10px; font-family: ${T.fontBody}; font-size: 14px; color: ${T.text}; background: transparent; border: 1px solid ${T.divider}; border-radius: ${T.radiusMd}; transition: border-color .15s; }
|
||||||
|
.legal-input:hover { border-color: rgba(32,31,29,0.45); }
|
||||||
|
.legal-input:focus { outline: none; border-color: ${T.accent}; }
|
||||||
|
.tag-outline { display: inline-flex; align-items: center; font-size: 10.5px; letter-spacing: 0.02em; padding: 3px 10px; border-radius: 3px; border: 1px solid ${T.accent}; color: ${T.accent}; }
|
||||||
|
@media (max-width: 900px) { .rail-fixed { display: none !important; } .hero-badges { display: none !important; } .card-grid { grid-template-columns: 1fr; } }
|
||||||
|
@media (max-width: 700px) { .hero-two-col { grid-template-columns: 1fr !important; } .contact-two-col { grid-template-columns: 1fr !important; } }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* ── Left rail ── */}
|
||||||
|
<div
|
||||||
|
className="rail-fixed"
|
||||||
|
style={{ position: 'fixed', left: '22px', top: 0, bottom: 0, width: '26px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '26px', zIndex: 20 }}
|
||||||
|
>
|
||||||
|
{RAIL_ITEMS.map(item => (
|
||||||
|
<a key={item.id} href={item.href} className="rail-link" style={{ color: 'rgba(32,31,29,0.4)', fontWeight: 400 }}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.color = T.accentDark; e.currentTarget.style.fontWeight = '600' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(32,31,29,0.4)'; e.currentTarget.style.fontWeight = '400' }}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ NAV ═════════════════════════════ */}
|
||||||
|
<nav style={{
|
||||||
|
position: 'sticky', top: 0, zIndex: 30,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
flexWrap: 'wrap', gap: '16px',
|
||||||
|
padding: '14px clamp(20px,6vw,64px)',
|
||||||
|
borderBottom: `1px solid ${T.divider}`,
|
||||||
|
background: T.bg,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<span style={{ fontFamily: T.fontHead, fontSize: '19px', textTransform: 'uppercase', display: 'flex', gap: '4px' }}>
|
||||||
|
<span style={{ fontWeight: 400, letterSpacing: '0.22em' }}>AYRIS</span>
|
||||||
|
<span style={{ fontWeight: 500, letterSpacing: '0.35em' }}>LEGAL</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '24px', flexWrap: 'wrap' }}>
|
||||||
|
<a href="#farklilasim" style={{ color: 'inherit', textDecoration: 'none', fontSize: '14px', fontFamily: T.fontBody }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.color = T.accent)}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.color = 'inherit')}
|
||||||
|
>Farklılaşım</a>
|
||||||
|
<a href="#ozellikler" style={{ color: 'inherit', textDecoration: 'none', fontSize: '14px', fontFamily: T.fontBody }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.color = T.accent)}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.color = 'inherit')}
|
||||||
|
>Özellikler</a>
|
||||||
|
<a href="#fiyatlandirma" style={{ color: 'inherit', textDecoration: 'none', fontSize: '14px', fontFamily: T.fontBody }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.color = T.accent)}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.color = 'inherit')}
|
||||||
|
>Fiyatlandırma</a>
|
||||||
|
<a href="#iletisim" className="btn-primary">Demo Talep Et</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ HERO ════════════════════════════ */}
|
||||||
|
<section style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(64px,11vw,132px) clamp(20px,6vw,72px) clamp(48px,7vw,80px)', position: 'relative', overflow: 'hidden' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'inline-block', fontSize: '13px', letterSpacing: '0.1em', textTransform: 'uppercase', color: T.accentDark, borderBottom: `1px solid ${T.accent}`, paddingBottom: '4px', marginBottom: '26px' }}>
|
||||||
|
Madde 01 — Değer Önermesi
|
||||||
|
</span>
|
||||||
|
</Reveal>
|
||||||
|
<div className="hero-two-col" style={{ display: 'grid', gridTemplateColumns: 'minmax(0,7fr) minmax(0,5fr)', gap: '56px', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<Reveal delay={0.05}>
|
||||||
|
<h1 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(46px,7.4vw,96px)', lineHeight: 0.98, letterSpacing: '-0.015em', margin: 0, marginLeft: '-0.03em', marginBottom: '0' }}>
|
||||||
|
<span style={{ display: 'block' }}>Dosyanızı okur,</span>
|
||||||
|
<span style={{ display: 'block' }}>sizinle <em style={{ fontStyle: 'italic', color: T.accentDark }}>tartışır.</em></span>
|
||||||
|
</h1>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.12}>
|
||||||
|
<p style={{ fontSize: 'clamp(17px,1.7vw,20px)', lineHeight: 1.65, maxWidth: '52ch', margin: '30px 0 0', color: 'rgba(32,31,29,0.82)', fontFamily: T.fontBody }}>
|
||||||
|
AyrisLegal, yüklediğiniz dava dosyasını analiz eden, içtihat bulan ve dilekçe taslağı çıkaran bir yapay zeka çalışma ortağı — pasif bir arama motoru değil, dosya üzerinde sizinle muhakeme eden bir meslektaş.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.18}>
|
||||||
|
<div style={{ display: 'flex', gap: '14px', flexWrap: 'wrap', marginTop: '36px', alignItems: 'center' }}>
|
||||||
|
<a href="#iletisim" className="btn-primary" style={{ fontSize: '15px', padding: '13px 24px' }}>Demo Talep Et</a>
|
||||||
|
<a href="#farklilasim" className="btn-ghost" style={{ fontSize: '15px' }}>Nasıl çalışır ↓</a>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '13px', marginTop: '24px', color: 'rgba(32,31,29,0.6)', fontFamily: T.fontBody }}>
|
||||||
|
Şu an erken erişim aşamasındayız — kurucu ekiple doğrudan görüşün.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero right: mock UI badges + plate */}
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div className="hero-badges" style={{ position: 'absolute', top: '-18px', right: '6px', zIndex: 2, background: T.bg, border: `1px solid ${T.divider}`, borderRadius: T.radiusMd, padding: '8px 14px', fontSize: '12.5px', boxShadow: T.shadowSm, transform: 'rotate(-2deg)', fontFamily: T.fontBody }}>
|
||||||
|
İçtihat bulundu · 3 emsal
|
||||||
|
</div>
|
||||||
|
<div className="hero-badges" style={{ position: 'absolute', bottom: '-16px', left: '-14px', zIndex: 2, background: T.bg, border: `1px solid ${T.divider}`, borderRadius: T.radiusMd, padding: '8px 14px', fontSize: '12.5px', boxShadow: T.shadowSm, transform: 'rotate(2deg)', fontFamily: T.fontBody }}>
|
||||||
|
Dilekçe taslağı hazır
|
||||||
|
</div>
|
||||||
|
{/* Placeholder plate */}
|
||||||
|
<div className="plate" style={{ width: '100%', aspectRatio: '4/5', background: T.surface, display: 'flex', flexDirection: 'column', gap: '12px', padding: '24px', justifyContent: 'center' }}>
|
||||||
|
{['Dava dosyası yüklendi', 'AI analiz ediyor…', 'Argüman değerlendirmesi', 'Emsal: Yargıtay 2024/1234', 'Dilekçe taslağı oluşturuldu'].map((step, i) => (
|
||||||
|
<div key={i} style={{ padding: '10px 14px', borderRadius: T.radiusMd, background: T.bg, border: `1px solid ${T.divider}`, fontSize: '13px', color: i === 4 ? T.accentDark : 'rgba(32,31,29,0.75)', fontFamily: T.fontBody, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<span style={{ width: '18px', height: '18px', borderRadius: '50%', background: i === 4 ? T.accent : T.divider, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px', color: i === 4 ? '#fff' : 'rgba(32,31,29,0.5)', flexShrink: 0, fontFamily: T.fontHead }}>
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
{step}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ STATS ═══════════════════════════ */}
|
||||||
|
<section style={{ maxWidth: '1240px', margin: '0 auto', padding: '0 clamp(20px,6vw,72px) clamp(56px,8vw,88px)', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px,1fr))', borderTop: `1px solid ${T.divider}`, borderBottom: `1px solid ${T.divider}` }}>
|
||||||
|
{[
|
||||||
|
{ value: '01', label: 'Sabit fiyat, tüm avukatlara' },
|
||||||
|
{ value: '40.000 TL', label: 'Yıllık, gizli ücret yok' },
|
||||||
|
{ value: '%100', label: "Veri Türkiye'de kalır" },
|
||||||
|
{ value: '7/24', label: 'Dosyanızla çalışan ortak' },
|
||||||
|
].map((s, i, arr) => (
|
||||||
|
<Reveal key={s.label} delay={i * 0.05}>
|
||||||
|
<div style={{ padding: '28px 20px', borderRight: i < arr.length - 1 ? `1px solid ${T.divider}` : undefined, textAlign: 'center' }}>
|
||||||
|
<div style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(30px,4vw,44px)', color: T.accentDark }}>{s.value}</div>
|
||||||
|
<div style={{ fontSize: '13px', marginTop: '6px', color: 'rgba(32,31,29,0.65)', letterSpacing: '0.02em' }}>{s.label}</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ PROBLEM ═════════════════════════ */}
|
||||||
|
<section id="is-akisi" style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(48px,8vw,96px) clamp(20px,6vw,72px)' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'block', fontSize: '13px', letterSpacing: '0.08em', textTransform: 'uppercase', color: T.accentDark, marginBottom: '14px' }}>
|
||||||
|
Madde 02 — Bugünün iş akışı
|
||||||
|
</span>
|
||||||
|
<h2 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(30px,3.6vw,44px)', margin: '0 0 44px', letterSpacing: '-0.008em', maxWidth: '24ch' }}>
|
||||||
|
Dosya, arama, şablon, tekrar.
|
||||||
|
</h2>
|
||||||
|
</Reveal>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(280px,1fr))', gap: 0, borderTop: `1px solid ${T.divider}` }}>
|
||||||
|
<Reveal delay={0.05}>
|
||||||
|
<div style={{ padding: '32px 28px 32px 0', borderRight: `1px solid ${T.divider}` }}>
|
||||||
|
<span style={{ fontFamily: T.fontHead, fontSize: '15px', color: 'rgba(32,31,29,0.45)' }}>01 / Bugün</span>
|
||||||
|
<p style={{ fontSize: '15.5px', lineHeight: 1.7, margin: '14px 0 0', textAlign: 'justify', hyphens: 'auto', color: 'rgba(32,31,29,0.78)' } as React.CSSProperties}>
|
||||||
|
Dosya incelemesi bir programda, içtihat araması ayrı bir siteye açılan sekmede, dilekçe taslağı eski bir Word şablonunda. Her adımda bağlam yeniden kurulur, saatler kaybolur.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<div style={{ padding: '32px 0 32px 28px' }}>
|
||||||
|
<span style={{ fontFamily: T.fontHead, fontSize: '15px', color: T.accentDark }}>02 / AyrisLegal ile</span>
|
||||||
|
<p style={{ fontSize: '15.5px', lineHeight: 1.7, margin: '14px 0 0', textAlign: 'justify', hyphens: 'auto', color: 'rgba(32,31,29,0.78)' } as React.CSSProperties}>
|
||||||
|
Tek bir çalışma alanı: dosyanızı yükleyin, AyrisLegal ile tartışın, içtihat bulun, dilekçe taslağı üretin — hepsi aynı ekranda, bağlamı hiç kaybetmeden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr className="hr-line" style={{ maxWidth: '1240px', margin: '0 auto' }} />
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ DIFFERENTIATOR ══════════════════ */}
|
||||||
|
<section id="farklilasim" style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(56px,9vw,108px) clamp(20px,6vw,72px)' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'block', fontSize: '13px', letterSpacing: '0.08em', textTransform: 'uppercase', color: T.accentDark, marginBottom: '14px' }}>
|
||||||
|
Madde 03 — Tartışma odaklı yapay zeka
|
||||||
|
</span>
|
||||||
|
</Reveal>
|
||||||
|
<div className="hero-two-col" style={{ display: 'grid', gridTemplateColumns: 'minmax(0,5fr) minmax(0,7fr)', gap: '56px', alignItems: 'center' }}>
|
||||||
|
<Reveal delay={0.05}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(28px,3.2vw,40px)', margin: '0 0 20px', letterSpacing: '-0.008em', maxWidth: '16ch' }}>
|
||||||
|
Bulmakla yetinmez, sizinle tartışır.
|
||||||
|
</h2>
|
||||||
|
<p style={{ fontSize: '15.5px', lineHeight: 1.7, margin: '0 0 16px', maxWidth: '46ch', textAlign: 'justify', hyphens: 'auto', color: 'rgba(32,31,29,0.78)' } as React.CSSProperties}>
|
||||||
|
Çoğu hukuk yazılımı özet çıkarır ya da anahtar kelimeyle arar. AyrisLegal dosyanızdaki argümanı okur, zayıf noktasını sorar, karşı görüş üretir, sizi ikna etmeye çalışır — ya da ikna olur.
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: '13px', margin: 0, color: 'rgba(32,31,29,0.58)' }}>
|
||||||
|
Aşağıdaki görsel, bu tartışmanın nasıl göründüğünü gösteren temsili bir tasarımdır.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Chat simulation */}
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<div className="plate" style={{ margin: 0, padding: '28px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||||
|
{/* Lawyer message */}
|
||||||
|
<div style={{ alignSelf: 'flex-end', maxWidth: '78%', background: T.bg, border: `1px solid ${T.divider}`, borderRadius: `${T.radiusMd} ${T.radiusMd} 4px ${T.radiusMd}`, padding: '14px 16px' }}>
|
||||||
|
<div style={{ fontSize: '11px', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'rgba(32,31,29,0.55)', marginBottom: '6px' }}>Avukat</div>
|
||||||
|
<p style={{ margin: 0, fontSize: '14.5px', lineHeight: 1.6, fontFamily: T.fontBody }}>Müvekkilin haklı olduğunu düşünüyorum, mülkiyet devri geçerli.</p>
|
||||||
|
</div>
|
||||||
|
{/* AyrisLegal response */}
|
||||||
|
<div style={{ alignSelf: 'flex-start', maxWidth: '82%', background: T.surface, border: `1px solid ${T.accent}`, borderRadius: `${T.radiusMd} ${T.radiusMd} ${T.radiusMd} 4px`, padding: '14px 16px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
|
||||||
|
<span style={{ fontSize: '11px', letterSpacing: '0.06em', textTransform: 'uppercase', color: T.accentDark }}>AyrisLegal</span>
|
||||||
|
<span className="tag-outline" style={{ fontSize: '10.5px' }}>İtiraz</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: 0, fontSize: '14.5px', lineHeight: 1.6, fontFamily: T.fontBody }}>Devir tarihinde tapuda şerh var — bu, iyi niyet iddianızı zayıflatabilir. Şerhin kaldırılış tarihini kontrol ettiniz mi?</p>
|
||||||
|
</div>
|
||||||
|
{/* Lawyer reply */}
|
||||||
|
<div style={{ alignSelf: 'flex-end', maxWidth: '78%', background: T.bg, border: `1px solid ${T.divider}`, borderRadius: `${T.radiusMd} ${T.radiusMd} 4px ${T.radiusMd}`, padding: '14px 16px' }}>
|
||||||
|
<p style={{ margin: 0, fontSize: '14.5px', lineHeight: 1.6, fontFamily: T.fontBody }}>Haklısınız — 3 emsal kararla bu noktayı destekleyelim.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr className="hr-line" style={{ maxWidth: '1240px', margin: '0 auto' }} />
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ FEATURES ════════════════════════ */}
|
||||||
|
<section id="ozellikler" style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(56px,9vw,108px) clamp(20px,6vw,72px)' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'block', fontSize: '13px', letterSpacing: '0.08em', textTransform: 'uppercase', color: T.accentDark, marginBottom: '14px' }}>
|
||||||
|
Madde 04 — Platform
|
||||||
|
</span>
|
||||||
|
<h2 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(30px,3.6vw,44px)', margin: '0 0 40px', letterSpacing: '-0.008em' }}>
|
||||||
|
Bir avukatın ihtiyaç duyduğu şeyler.
|
||||||
|
</h2>
|
||||||
|
</Reveal>
|
||||||
|
<div className="card-grid">
|
||||||
|
{/* Main feature — spans 2 cols */}
|
||||||
|
<Reveal>
|
||||||
|
<div className="card" style={{ gridColumn: 'span 2' }}>
|
||||||
|
<span className="card-kicker">AI tartışma ortağı</span>
|
||||||
|
<h3 className="card-title" style={{ fontSize: '26px' }}>Argümanınızı sınar, meslektaş gibi</h3>
|
||||||
|
<p className="card-body">Zayıf noktaları sorar, karşı görüş üretir, sizi zorlar — pasif özet değil, aktif muhakeme. AyrisLegal'ın temel farkı budur.</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.05}>
|
||||||
|
<div className="card">
|
||||||
|
<span className="card-kicker">Dosya analizi</span>
|
||||||
|
<h3 className="card-title">Dava dosyasını okur</h3>
|
||||||
|
<p className="card-body">Olay örgüsünü, tarafları ve hukuki dayanakları çıkarır.</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<div className="card">
|
||||||
|
<span className="card-kicker">İçtihat arama</span>
|
||||||
|
<h3 className="card-title">Bağlama uygun emsal bulur</h3>
|
||||||
|
<p className="card-body">Anahtar kelime eşleşmesiyle değil, hukuki benzerlikle emsal kararları bulur.</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.12}>
|
||||||
|
<div className="card">
|
||||||
|
<span className="card-kicker">Dilekçe taslağı</span>
|
||||||
|
<h3 className="card-title">Tartışmayı taslağa döker</h3>
|
||||||
|
<p className="card-body">Vardığınız sonucu, düzenlenebilir bir dilekçe taslağına dönüştürür.</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.15}>
|
||||||
|
<div className="card">
|
||||||
|
<span className="card-kicker" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
Büro paylaşımı <span className="tag-outline">Yakında</span>
|
||||||
|
</span>
|
||||||
|
<h3 className="card-title">Ekip çalışması</h3>
|
||||||
|
<p className="card-body">Dosya ve tartışma geçmişini büro içindeki meslektaşlarınızla paylaşın.</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr className="hr-line" style={{ maxWidth: '1240px', margin: '0 auto' }} />
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ PRICING ═════════════════════════ */}
|
||||||
|
<section id="fiyatlandirma" style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(56px,9vw,108px) clamp(20px,6vw,72px)' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'block', fontSize: '13px', letterSpacing: '0.08em', textTransform: 'uppercase', color: T.accentDark, marginBottom: '14px', textAlign: 'center' }}>
|
||||||
|
Madde 05 — Fiyatlandırma
|
||||||
|
</span>
|
||||||
|
<h2 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(30px,3.6vw,44px)', margin: '0 0 44px', textAlign: 'center', letterSpacing: '-0.008em' }}>
|
||||||
|
İhtiyacınıza uygun modeller.
|
||||||
|
</h2>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: '32px', maxWidth: '960px', margin: '0 auto' }}>
|
||||||
|
|
||||||
|
{/* ── Bireysel ── */}
|
||||||
|
<Reveal delay={0.08}>
|
||||||
|
<div style={{ display: 'flex', border: `1px solid ${T.divider}`, borderRadius: T.radiusLg, boxShadow: T.shadowMd, overflow: 'hidden', height: '100%' }}>
|
||||||
|
<div style={{ width: '34px', background: T.neutral100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'space-evenly', borderRight: `1px solid ${T.divider}`, padding: '20px 0' }}>
|
||||||
|
{['01','02','03','04','05'].map(t => (
|
||||||
|
<span key={t} style={{ fontSize: '11px', color: T.accentDark, writingMode: 'vertical-rl', fontFamily: T.fontHead }}>{t}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, padding: '40px 36px', textAlign: 'center', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<p style={{ margin: 0, fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(42px,5vw,56px)', lineHeight: 1 }}>40.000 TL</p>
|
||||||
|
<p style={{ margin: '6px 0 0', fontSize: '14px', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'rgba(32,31,29,0.6)' }}>Bireysel Avukat / Yıl</p>
|
||||||
|
<p style={{ fontSize: '15px', lineHeight: 1.6, margin: '20px auto 0', color: 'rgba(32,31,29,0.78)', fontFamily: T.fontBody }}>
|
||||||
|
Serbest çalışan avukatlar için tasarlanmış yapay zeka çalışma ortağı.
|
||||||
|
</p>
|
||||||
|
<hr className="hr-line" style={{ margin: '24px 0' }} />
|
||||||
|
<ul style={{ listStyle: 'none', margin: '0 0 auto 0', padding: 0, display: 'flex', flexDirection: 'column', gap: '12px', textAlign: 'left' }}>
|
||||||
|
{['Sınırsız dosya analizi', 'AI tartışma ortağı', 'İçtihat arama', 'Dilekçe taslağı üretimi'].map(item => (
|
||||||
|
<li key={item} style={{ display: 'flex', gap: '10px', fontSize: '14.5px', fontFamily: T.fontBody }}>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.accent} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, marginTop: '2px' }}>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a href="#iletisim" className="btn-primary-lg" style={{ marginTop: '26px' }}>Demo Talep Et</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* ── Büro ── */}
|
||||||
|
<Reveal delay={0.15}>
|
||||||
|
<div style={{ display: 'flex', border: `1px solid ${T.accent}`, borderRadius: T.radiusLg, boxShadow: T.shadowMd, overflow: 'hidden', height: '100%', position: 'relative' }}>
|
||||||
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, background: T.accent, color: '#fff', fontSize: '11px', letterSpacing: '0.1em', textTransform: 'uppercase', textAlign: 'center', padding: '4px', fontFamily: T.fontHead, fontWeight: 600 }}>Ekip Çalışması</div>
|
||||||
|
<div style={{ width: '34px', background: 'rgba(182,130,53,0.05)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'space-evenly', borderRight: `1px solid ${T.divider}`, padding: '40px 0 20px' }}>
|
||||||
|
{['01','02','03','04','05'].map(t => (
|
||||||
|
<span key={t} style={{ fontSize: '11px', color: T.accentDark, writingMode: 'vertical-rl', fontFamily: T.fontHead }}>{t}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, padding: '56px 36px 40px', textAlign: 'center', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<p style={{ margin: 0, fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(32px,4vw,42px)', lineHeight: 1.1, color: T.accentDark, marginTop: '6px' }}>Hukuk Bürosu</p>
|
||||||
|
<p style={{ margin: '10px 0 0', fontSize: '14px', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'rgba(32,31,29,0.6)' }}>Özel Fiyatlandırma</p>
|
||||||
|
<p style={{ fontSize: '15px', lineHeight: 1.6, margin: '20px auto 0', color: 'rgba(32,31,29,0.78)', fontFamily: T.fontBody }}>
|
||||||
|
Tüm ekibiniz için paylaşımlı çalışma alanı, dosya yönetimi ve özel entegrasyonlar.
|
||||||
|
</p>
|
||||||
|
<hr className="hr-line" style={{ margin: '24px 0' }} />
|
||||||
|
<ul style={{ listStyle: 'none', margin: '0 0 auto 0', padding: 0, display: 'flex', flexDirection: 'column', gap: '12px', textAlign: 'left' }}>
|
||||||
|
{['Tüm bireysel özellikler', 'Ekip içi dosya paylaşımı', 'Ortak içtihat havuzu', 'Kurucu ekiple özel destek'].map(item => (
|
||||||
|
<li key={item} style={{ display: 'flex', gap: '10px', fontSize: '14.5px', fontFamily: T.fontBody }}>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.accentDark} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, marginTop: '2px' }}>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a href="#iletisim" className="btn-primary-lg" style={{ marginTop: '26px', background: T.accent, color: '#fff', border: 'none' }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.background = T.accentDark)}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.background = T.accent)}
|
||||||
|
>İletişime Geçin</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr className="hr-line" style={{ maxWidth: '1240px', margin: '0 auto' }} />
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ TRUST ═══════════════════════════ */}
|
||||||
|
<section id="guven" style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(56px,9vw,108px) clamp(20px,6vw,72px)' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'block', fontSize: '13px', letterSpacing: '0.08em', textTransform: 'uppercase', color: T.accentDark, marginBottom: '14px' }}>
|
||||||
|
Madde 06 — Güven ve veri güvenliği
|
||||||
|
</span>
|
||||||
|
<h2 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(30px,3.6vw,44px)', margin: '0 0 16px', letterSpacing: '-0.008em' }}>
|
||||||
|
Dosyalarınız sizin kalır.
|
||||||
|
</h2>
|
||||||
|
<p style={{ fontSize: '15.5px', lineHeight: 1.7, maxWidth: '60ch', margin: '0 0 40px', color: 'rgba(32,31,29,0.78)', fontFamily: T.fontBody }}>
|
||||||
|
Bir dava dosyası, bir avukatın elindeki en hassas belgedir. AyrisLegal'ı bu bilinçle kuruyoruz.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(240px,1fr))', gap: 0, borderTop: `1px solid ${T.divider}` }}>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke={T.accent} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2 L20 5 V11 C20 16 16.5 19.5 12 21 C7.5 19.5 4 16 4 11 V5 Z"/><path d="M9 12 l2.2 2.2 L15.5 9.5"/></svg>,
|
||||||
|
title: "KVKK'ya uyumlu",
|
||||||
|
desc: 'Kişisel verilerin korunması mevzuatına uygun işleme ve saklama.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke={T.accent} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/><line x1="7" y1="7" x2="7" y2="7"/><line x1="7" y1="17" x2="7" y2="17"/></svg>,
|
||||||
|
title: "Sunucular Türkiye'de",
|
||||||
|
desc: "Verileriniz yurt dışına çıkmaz — Ayris Tech'in self-hosted altyapı felsefesiyle.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke={T.accent} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="10" width="14" height="10" rx="1.5"/><path d="M8 10 V7 a4 4 0 0 1 8 0 v3"/></svg>,
|
||||||
|
title: 'Dosyanız modelleri eğitmez',
|
||||||
|
desc: 'Yüklediğiniz belgeler yalnızca sizin analizinizde kullanılır.',
|
||||||
|
},
|
||||||
|
].map((item, i, arr) => (
|
||||||
|
<Reveal key={item.title} delay={i * 0.08}>
|
||||||
|
<div style={{ padding: '28px 24px 0', borderRight: i < arr.length - 1 ? `1px solid ${T.divider}` : undefined }}>
|
||||||
|
{item.icon}
|
||||||
|
<h3 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: '19px', margin: '12px 0 6px' }}>{item.title}</h3>
|
||||||
|
<p style={{ fontSize: '14.5px', lineHeight: 1.6, margin: 0, color: 'rgba(32,31,29,0.78)', fontFamily: T.fontBody }}>{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr className="hr-line" style={{ maxWidth: '1240px', margin: '0 auto' }} />
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ CONTACT ═════════════════════════ */}
|
||||||
|
<section id="iletisim" style={{ maxWidth: '1240px', margin: '0 auto', padding: 'clamp(56px,9vw,108px) clamp(20px,6vw,72px)' }}>
|
||||||
|
<Reveal>
|
||||||
|
<span style={{ display: 'block', fontSize: '13px', letterSpacing: '0.08em', textTransform: 'uppercase', color: T.accentDark, marginBottom: '14px' }}>
|
||||||
|
Madde 07 — Erken erişim
|
||||||
|
</span>
|
||||||
|
</Reveal>
|
||||||
|
<div className="contact-two-col" style={{ display: 'grid', gridTemplateColumns: 'minmax(0,5fr) minmax(0,7fr)', gap: '48px' }}>
|
||||||
|
<Reveal delay={0.05}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: 'clamp(28px,3.2vw,38px)', margin: '0 0 16px', letterSpacing: '-0.008em' }}>
|
||||||
|
Şu an müşteri referansı göstermiyoruz — kasıtlı olarak.
|
||||||
|
</h2>
|
||||||
|
<p style={{ fontSize: '15.5px', lineHeight: 1.7, margin: 0, textAlign: 'justify', hyphens: 'auto', color: 'rgba(32,31,29,0.78)', fontFamily: T.fontBody } as React.CSSProperties}>
|
||||||
|
AyrisLegal yeni. Sahte ya da ödünç yorumlar yerine dürüst olmayı seçtik: platformu kurucu ekiple birlikte, doğrudan geri bildiriminizle şekillendiriyoruz. Demo talep edin, ilk kullanan avukatlardan biri olun.
|
||||||
|
</p>
|
||||||
|
<div style={{ marginTop: '32px', display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||||
|
<div style={{ width: '48px', height: '48px', border: `1px solid ${T.accent}`, borderRadius: '50%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontFamily: T.fontHead, fontSize: '8px', color: T.accentDark, textAlign: 'center', lineHeight: 1.2, flexShrink: 0, textTransform: 'uppercase' }}>
|
||||||
|
<span style={{ fontWeight: 400, letterSpacing: '0.22em' }}>AYRIS</span>
|
||||||
|
<span style={{ fontWeight: 500, letterSpacing: '0.35em' }}>LEGAL</span>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '12.5px', color: 'rgba(32,31,29,0.6)', fontFamily: T.fontBody }}>Kurucu ekip mührü</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<ContactForm />
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ══════════════════════════════ FOOTER ══════════════════════════ */}
|
||||||
|
<footer style={{ maxWidth: '1240px', margin: '0 auto', padding: '28px clamp(20px,6vw,72px) 40px', display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '12px', fontSize: '13px', color: 'rgba(32,31,29,0.6)', borderTop: `1px solid ${T.divider}`, fontFamily: T.fontBody }}>
|
||||||
|
<span>AyrisLegal, Ayris Tech'in bir ürünüdür.</span>
|
||||||
|
<span>
|
||||||
|
© {new Date().getFullYear()} AyrisLegal ·{' '}
|
||||||
|
<a href="https://ayris.tech" target="_blank" rel="noopener noreferrer" style={{ color: 'rgba(32,31,29,0.6)' }}>
|
||||||
|
Created by ayris.tech
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { handlers } from "@/lib/auth"
|
||||||
|
|
||||||
|
export const { GET, POST } = handlers
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
|
||||||
|
// Basit in-memory rate limiting (MVP — production'da Redis kullan)
|
||||||
|
const ipRequestMap = new Map<string, { count: number; resetAt: number }>()
|
||||||
|
const RATE_LIMIT = 5 // maksimum istek
|
||||||
|
const WINDOW_MS = 60_000 // 1 dakika
|
||||||
|
|
||||||
|
function checkRateLimit(ip: string): boolean {
|
||||||
|
const now = Date.now()
|
||||||
|
const entry = ipRequestMap.get(ip)
|
||||||
|
|
||||||
|
if (!entry || now > entry.resetAt) {
|
||||||
|
ipRequestMap.set(ip, { count: 1, resetAt: now + WINDOW_MS })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.count >= RATE_LIMIT) return false
|
||||||
|
|
||||||
|
entry.count++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// IP tespiti
|
||||||
|
const forwarded = request.headers.get('x-forwarded-for')
|
||||||
|
const ip = forwarded ? forwarded.split(',')[0].trim() : 'unknown'
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
if (!checkRateLimit(ip)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Çok fazla istek. Lütfen bir dakika bekleyip tekrar deneyin.' },
|
||||||
|
{ status: 429 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const { name, email, barNo, message, _hp } = body
|
||||||
|
|
||||||
|
// Honeypot kontrolü
|
||||||
|
if (_hp) {
|
||||||
|
// Bot — 200 döndür ama işleme
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zorunlu alan doğrulaması
|
||||||
|
if (!name || typeof name !== 'string' || name.trim().length < 2) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Ad Soyad zorunludur (en az 2 karakter).' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email || typeof email !== 'string' || !EMAIL_REGEX.test(email.trim())) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Geçerli bir e-posta adresi giriniz.' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MVP: Konsola yaz
|
||||||
|
// TODO: Resend/SendGrid veya Prisma leads tablosu eklenebilir
|
||||||
|
console.log('[AyrisLegal] 🎯 Yeni Demo Talebi:', {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
name: name.trim(),
|
||||||
|
email: email.trim().toLowerCase(),
|
||||||
|
barNo: barNo?.trim() || null,
|
||||||
|
message: message?.trim() || null,
|
||||||
|
ip,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[AyrisLegal] Contact API hatası:', err)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Sunucu hatası oluştu.' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,198 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-archivo);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--font-heading: var(--font-archivo);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── AyrisLegal Marka Renk Sistemi (Lacivert + Teal) ─────────────── */
|
||||||
|
:root {
|
||||||
|
/* Lacivert (navy) arka plan / teal vurgu — dark-first marka */
|
||||||
|
--background: oklch(0.18 0.055 243); /* Derin lacivert */
|
||||||
|
--foreground: oklch(0.95 0.012 240); /* Neredeyse beyaz */
|
||||||
|
|
||||||
|
--card: oklch(0.22 0.05 243); /* Biraz açık lacivert kart */
|
||||||
|
--card-foreground: oklch(0.95 0.012 240);
|
||||||
|
|
||||||
|
--popover: oklch(0.22 0.05 243);
|
||||||
|
--popover-foreground: oklch(0.95 0.012 240);
|
||||||
|
|
||||||
|
/* Teal ana renk */
|
||||||
|
--primary: oklch(0.63 0.14 183); /* Teal */
|
||||||
|
--primary-foreground: oklch(0.98 0.008 183);
|
||||||
|
|
||||||
|
--secondary: oklch(0.28 0.05 243); /* Orta lacivert */
|
||||||
|
--secondary-foreground: oklch(0.90 0.012 240);
|
||||||
|
|
||||||
|
--muted: oklch(0.25 0.04 243);
|
||||||
|
--muted-foreground: oklch(0.60 0.035 240);
|
||||||
|
|
||||||
|
--accent: oklch(0.68 0.13 183); /* Açık teal vurgu */
|
||||||
|
--accent-foreground: oklch(0.18 0.055 243);
|
||||||
|
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
|
||||||
|
--border: oklch(0.32 0.045 243);
|
||||||
|
--input: oklch(0.28 0.045 243);
|
||||||
|
--ring: oklch(0.63 0.14 183); /* Teal focus ring */
|
||||||
|
|
||||||
|
--chart-1: oklch(0.63 0.14 183);
|
||||||
|
--chart-2: oklch(0.55 0.12 200);
|
||||||
|
--chart-3: oklch(0.45 0.09 220);
|
||||||
|
--chart-4: oklch(0.38 0.07 230);
|
||||||
|
--chart-5: oklch(0.30 0.05 240);
|
||||||
|
|
||||||
|
--radius: 0.75rem;
|
||||||
|
|
||||||
|
--sidebar: oklch(0.20 0.055 243);
|
||||||
|
--sidebar-foreground: oklch(0.90 0.012 240);
|
||||||
|
--sidebar-primary: oklch(0.63 0.14 183);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.008 183);
|
||||||
|
--sidebar-accent: oklch(0.25 0.05 243);
|
||||||
|
--sidebar-accent-foreground: oklch(0.90 0.012 240);
|
||||||
|
--sidebar-border: oklch(0.30 0.045 243);
|
||||||
|
--sidebar-ring: oklch(0.63 0.14 183);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin/login sayfaları için açık tema (opsiyonel override) */
|
||||||
|
.light {
|
||||||
|
--background: oklch(0.98 0.005 240);
|
||||||
|
--foreground: oklch(0.18 0.055 243);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.18 0.055 243);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.18 0.055 243);
|
||||||
|
--primary: oklch(0.55 0.14 183);
|
||||||
|
--primary-foreground: oklch(0.98 0.008 183);
|
||||||
|
--secondary: oklch(0.93 0.02 240);
|
||||||
|
--secondary-foreground: oklch(0.22 0.05 243);
|
||||||
|
--muted: oklch(0.95 0.015 240);
|
||||||
|
--muted-foreground: oklch(0.50 0.04 240);
|
||||||
|
--accent: oklch(0.90 0.06 183);
|
||||||
|
--accent-foreground: oklch(0.25 0.05 243);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.88 0.02 240);
|
||||||
|
--input: oklch(0.88 0.02 240);
|
||||||
|
--ring: oklch(0.55 0.14 183);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── AyrisLegal Premium Background Effects ─────────────────────────── */
|
||||||
|
|
||||||
|
.orb {
|
||||||
|
position: fixed;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(110px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-1 {
|
||||||
|
width: 700px;
|
||||||
|
height: 700px;
|
||||||
|
background: radial-gradient(circle, rgba(45,212,191,0.22) 0%, transparent 70%);
|
||||||
|
top: -180px;
|
||||||
|
left: -120px;
|
||||||
|
animation: orb-float 14s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-2 {
|
||||||
|
width: 550px;
|
||||||
|
height: 550px;
|
||||||
|
background: radial-gradient(circle, rgba(99,102,241,0.16) 0%, transparent 70%);
|
||||||
|
top: 38%;
|
||||||
|
right: -120px;
|
||||||
|
animation: orb-float 18s ease-in-out infinite reverse;
|
||||||
|
animation-delay: -6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-3 {
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
background: radial-gradient(circle, rgba(45,212,191,0.12) 0%, transparent 70%);
|
||||||
|
bottom: 15%;
|
||||||
|
left: 28%;
|
||||||
|
animation: orb-float 11s ease-in-out infinite;
|
||||||
|
animation-delay: -9s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes orb-float {
|
||||||
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||||
|
33% { transform: translate(35px, -50px) scale(1.06); }
|
||||||
|
66% { transform: translate(-25px, 25px) scale(0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,0.022) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.022) 1px, transparent 1px);
|
||||||
|
background-size: 60px 60px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient text utility */
|
||||||
|
.text-gradient-teal {
|
||||||
|
background: linear-gradient(135deg, #2DD4BF 0%, #67E8F9 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "base-nova",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type FormState = 'idle' | 'loading' | 'success' | 'error'
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
bg: '#f3f2f2',
|
||||||
|
surface: '#eae9e9',
|
||||||
|
text: '#201f1d',
|
||||||
|
divider: 'rgba(32,31,29,0.16)',
|
||||||
|
accent: '#b68235',
|
||||||
|
accentDark: '#7d5411',
|
||||||
|
fontHead: '"Sora", sans-serif',
|
||||||
|
fontBody: '"Inter", sans-serif',
|
||||||
|
radiusMd: '4px',
|
||||||
|
radiusLg: '7px',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContactForm() {
|
||||||
|
const [state, setState] = useState<FormState>('idle')
|
||||||
|
const [form, setForm] = useState({ name: '', org: '', email: '', phone: '', _hp: '' })
|
||||||
|
|
||||||
|
const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setForm(p => ({ ...p, [k]: e.target.value }))
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (form._hp) return
|
||||||
|
setState('loading')
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: form.name, email: form.email, barNo: form.org, message: form.phone }),
|
||||||
|
})
|
||||||
|
setState(res.ok ? 'success' : 'error')
|
||||||
|
} catch {
|
||||||
|
setState('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
if (state === 'success') {
|
||||||
|
return (
|
||||||
|
<div style={{ border: `1px solid ${T.divider}`, borderRadius: T.radiusLg, padding: '40px', textAlign: 'center', background: T.surface }}>
|
||||||
|
<h3 style={{ fontFamily: T.fontHead, fontWeight: 400, fontSize: '22px', margin: '0 0 8px' }}>Talebiniz alındı.</h3>
|
||||||
|
<p style={{ fontSize: '14.5px', margin: 0, color: 'rgba(32,31,29,0.78)', fontFamily: T.fontBody }}>
|
||||||
|
Kurucu ekipten kısa süre içinde size dönüş yapılacak.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputStyle: React.CSSProperties = {
|
||||||
|
width: '100%', minHeight: '36px', padding: '6px 10px',
|
||||||
|
fontFamily: T.fontBody, fontSize: '14px', color: T.text,
|
||||||
|
background: 'transparent',
|
||||||
|
border: `1px solid ${T.divider}`, borderRadius: T.radiusMd,
|
||||||
|
transition: 'border-color .15s',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelStyle: React.CSSProperties = {
|
||||||
|
display: 'block', fontSize: '12px', marginBottom: '5px',
|
||||||
|
color: 'rgba(32,31,29,0.7)', fontFamily: T.fontBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit,minmax(200px,1fr))',
|
||||||
|
gap: '16px',
|
||||||
|
border: `1px solid ${T.divider}`,
|
||||||
|
borderRadius: T.radiusLg,
|
||||||
|
padding: '28px',
|
||||||
|
background: T.bg,
|
||||||
|
}}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
{/* Honeypot */}
|
||||||
|
<div aria-hidden="true" style={{ display: 'none' }} tabIndex={-1}>
|
||||||
|
<input type="text" name="_hp" value={form._hp} onChange={set('_hp')} autoComplete="off" tabIndex={-1} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ gridColumn: 'span 1' }}>
|
||||||
|
<label style={labelStyle} htmlFor="af_name">Ad Soyad <span style={{ color: T.accent }}>*</span></label>
|
||||||
|
<input
|
||||||
|
id="af_name" type="text" required
|
||||||
|
placeholder="Av. Ayşe Yılmaz"
|
||||||
|
value={form.name} onChange={set('name')}
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={e => (e.target.style.borderColor = T.accent)}
|
||||||
|
onBlur={e => (e.target.style.borderColor = T.divider)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle} htmlFor="af_org">Büro / Kurum</label>
|
||||||
|
<input
|
||||||
|
id="af_org" type="text"
|
||||||
|
placeholder="Yılmaz Hukuk Bürosu"
|
||||||
|
value={form.org} onChange={set('org')}
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={e => (e.target.style.borderColor = T.accent)}
|
||||||
|
onBlur={e => (e.target.style.borderColor = T.divider)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle} htmlFor="af_email">E-posta <span style={{ color: T.accent }}>*</span></label>
|
||||||
|
<input
|
||||||
|
id="af_email" type="email" required
|
||||||
|
placeholder="ayse@buro.com"
|
||||||
|
value={form.email} onChange={set('email')}
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={e => (e.target.style.borderColor = T.accent)}
|
||||||
|
onBlur={e => (e.target.style.borderColor = T.divider)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle} htmlFor="af_phone">Telefon</label>
|
||||||
|
<input
|
||||||
|
id="af_phone" type="tel"
|
||||||
|
placeholder="0532 000 00 00"
|
||||||
|
value={form.phone} onChange={set('phone')}
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={e => (e.target.style.borderColor = T.accent)}
|
||||||
|
onBlur={e => (e.target.style.borderColor = T.divider)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state === 'error' && (
|
||||||
|
<div style={{ gridColumn: '1 / -1', fontSize: '13px', color: '#c0392b', fontFamily: T.fontBody }}>
|
||||||
|
Bir hata oluştu. Lütfen tekrar deneyin veya{' '}
|
||||||
|
<a href="mailto:info@ayris.tech" style={{ color: T.accent }}>info@ayris.tech</a> adresine yazın.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="contact_submit"
|
||||||
|
type="submit"
|
||||||
|
disabled={state === 'loading'}
|
||||||
|
style={{
|
||||||
|
gridColumn: '1 / -1',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px',
|
||||||
|
cursor: state === 'loading' ? 'not-allowed' : 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontFamily: T.fontHead, fontWeight: 600, fontSize: '15px', lineHeight: 1.2,
|
||||||
|
color: T.accent,
|
||||||
|
background: 'transparent',
|
||||||
|
border: `1px solid ${T.accent}`,
|
||||||
|
padding: '12px',
|
||||||
|
borderRadius: T.radiusMd,
|
||||||
|
transition: 'background .15s',
|
||||||
|
opacity: state === 'loading' ? 0.6 : 1,
|
||||||
|
marginTop: '4px',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (state !== 'loading') (e.currentTarget as HTMLElement).style.background = 'rgba(182,130,53,0.10)' }}
|
||||||
|
onMouseLeave={e => { if (state !== 'loading') (e.currentTarget as HTMLElement).style.background = 'transparent' }}
|
||||||
|
>
|
||||||
|
{state === 'loading' ? 'Gönderiliyor…' : 'Demo Talep Et'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
icon: "size-8",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm":
|
||||||
|
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||||
|
"icon-lg": "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||||
|
return (
|
||||||
|
<ButtonPrimitive
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-(--card-spacing)", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<InputPrimitive
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="./support.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<x-dc>
|
||||||
|
<helmet>
|
||||||
|
<link rel="stylesheet" href="_ds/classical-911697ea-3fa0-408c-bdf6-3bbc9cae7b96/styles.css">
|
||||||
|
<script src="_ds/classical-911697ea-3fa0-408c-bdf6-3bbc9cae7b96/_ds_bundle.js"></script>
|
||||||
|
<script src="./image-slot.js"></script>
|
||||||
|
<style>
|
||||||
|
body{margin:0;text-wrap:pretty}
|
||||||
|
a{color:var(--color-accent);text-underline-offset:3px}
|
||||||
|
a:hover{color:var(--color-accent-700)}
|
||||||
|
@media (max-width: 980px){ .rail{display:none} .hero-badges{display:none} }
|
||||||
|
</style>
|
||||||
|
</helmet>
|
||||||
|
|
||||||
|
<div style="background:var(--color-bg);color:var(--color-text);font-family:var(--font-body);min-height:100vh;position:relative">
|
||||||
|
|
||||||
|
<div class="rail" style="position:fixed;left:22px;top:0;bottom:0;width:26px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:26px;z-index:20;font-feature-settings:'tnum' 1">
|
||||||
|
<sc-for list="{{ railItems }}" as="it" hint-placeholder-count="6">
|
||||||
|
<a href="{{ it.href }}" style="text-decoration:none;writing-mode:vertical-rl;font-size:11px;letter-spacing:0.08em;color:{{ it.color }};font-weight:{{ it.weight }};transition:color .2s">{{ it.label }}</a>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav style="position:sticky;top:0;z-index:30;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;padding:{{ navPad }};border-bottom:1px solid var(--color-divider);background:{{ navBg }};backdrop-filter:blur(10px);transition:padding .25s,background .25s">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
|
<image-slot id="logo-mark" shape="rect" style="width:34px;height:34px" placeholder="Logo" radius="4"></image-slot>
|
||||||
|
<span style="font-family:var(--font-heading);font-weight:600;font-size:19px;letter-spacing:-0.01em">AyrisLegal</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:24px;flex-wrap:wrap">
|
||||||
|
<a href="#farklilasim" style="color:inherit;text-decoration:none;font-size:14px">Farklılaşım</a>
|
||||||
|
<a href="#ozellikler" style="color:inherit;text-decoration:none;font-size:14px">Özellikler</a>
|
||||||
|
<a href="#fiyatlandirma" style="color:inherit;text-decoration:none;font-size:14px">Fiyatlandırma</a>
|
||||||
|
<a href="#iletisim" class="btn btn-primary">Demo Talep Et</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section style="max-width:1240px;margin:0 auto;padding:clamp(64px,11vw,132px) clamp(20px,6vw,72px) clamp(48px,7vw,80px);position:relative;overflow:hidden">
|
||||||
|
<span style="display:inline-block;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;color:var(--color-accent-700);border-bottom:1px solid var(--color-accent);padding-bottom:4px;font-feature-settings:'tnum' 1;margin-bottom:26px">Madde 01 — Değer Önermesi</span>
|
||||||
|
<div style="display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:56px;align-items:center">
|
||||||
|
<div>
|
||||||
|
<h1 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(46px,7.4vw,96px);line-height:0.98;letter-spacing:-0.015em;margin:0;margin-left:-0.03em">
|
||||||
|
<span style="display:block">Dosyanızı okur,</span>
|
||||||
|
<span style="display:block">sizinle <em style="font-style:italic;color:var(--color-accent-700)">tartışır.</em></span>
|
||||||
|
</h1>
|
||||||
|
<p style="font-size:clamp(17px,1.7vw,20px);line-height:1.65;max-width:52ch;margin:30px 0 0;color:color-mix(in srgb, var(--color-text) 82%, transparent)">
|
||||||
|
AyrisLegal, yüklediğiniz dava dosyasını analiz eden, içtihat bulan ve dilekçe taslağı çıkaran bir yapay zeka çalışma ortağı — pasif bir arama motoru değil, dosya üzerinde sizinle muhakeme eden bir meslektaş.
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;gap:14px;flex-wrap:wrap;margin-top:36px;align-items:center">
|
||||||
|
<a href="#iletisim" class="btn btn-primary" style="font-size:15px;padding:13px 24px">Demo Talep Et</a>
|
||||||
|
<a href="#farklilasim" class="btn btn-ghost" style="font-size:15px">Nasıl çalışır ↓</a>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:13px;margin-top:24px;color:color-mix(in srgb, var(--color-text) 60%, transparent)">Şu an erken erişim aşamasındayız — kurucu ekiple doğrudan görüşün.</p>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative">
|
||||||
|
<div class="hero-badges" style="position:absolute;top:-18px;right:6px;z-index:2;background:var(--color-bg);border:1px solid var(--color-divider);border-radius:var(--radius-md);padding:8px 14px;font-size:12.5px;box-shadow:var(--shadow-sm);transform:rotate(-2deg)">İçtihat bulundu · 3 emsal</div>
|
||||||
|
<div class="hero-badges" style="position:absolute;bottom:-16px;left:-14px;z-index:2;background:var(--color-bg);border:1px solid var(--color-divider);border-radius:var(--radius-md);padding:8px 14px;font-size:12.5px;box-shadow:var(--shadow-sm);transform:rotate(2deg)">Dilekçe taslağı hazır</div>
|
||||||
|
<figure class="plate" style="margin:0">
|
||||||
|
<image-slot id="hero-plate" shape="rect" radius="2" style="width:100%;aspect-ratio:4/5" placeholder="Temsili çalışma ekranı görseli"></image-slot>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="max-width:1240px;margin:0 auto;padding:0 clamp(20px,6vw,72px) clamp(56px,8vw,88px);display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));border-top:1px solid var(--color-divider);border-bottom:1px solid var(--color-divider)">
|
||||||
|
<sc-for list="{{ stats }}" as="s" hint-placeholder-count="4">
|
||||||
|
<div style="padding:28px 20px;border-right:1px solid var(--color-divider);text-align:center">
|
||||||
|
<div style="font-family:var(--font-heading);font-weight:400;font-size:clamp(30px,4vw,44px);font-feature-settings:'tnum' 1;color:var(--color-accent-700)">{{ s.value }}</div>
|
||||||
|
<div style="font-size:13px;margin-top:6px;color:color-mix(in srgb, var(--color-text) 65%, transparent);letter-spacing:0.02em">{{ s.label }}</div>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-reveal="problem" style="max-width:1240px;margin:0 auto;padding:clamp(48px,8vw,96px) clamp(20px,6vw,72px);opacity:{{ reveal.problem.opacity }};transform:{{ reveal.problem.transform }};transition:opacity .7s ease,transform .7s ease">
|
||||||
|
<span style="display:block;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent-700);font-feature-settings:'tnum' 1;margin-bottom:14px">Madde 02 — Bugünün iş akışı</span>
|
||||||
|
<h2 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(30px,3.6vw,44px);margin:0 0 44px;letter-spacing:-0.008em;max-width:24ch">Dosya, arama, şablon, tekrar.</h2>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:0;border-top:1px solid var(--color-divider)">
|
||||||
|
<div style="padding:32px 28px 32px 0;border-right:1px solid var(--color-divider)">
|
||||||
|
<span style="font-family:var(--font-heading);font-size:15px;color:color-mix(in srgb, var(--color-text) 45%, transparent);font-feature-settings:'tnum' 1">01 / Bugün</span>
|
||||||
|
<p style="font-size:15.5px;line-height:1.7;margin:14px 0 0;text-align:justify;hyphens:auto;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Dosya incelemesi bir programda, içtihat araması ayrı bir siteye açılan sekmede, dilekçe taslağı eski bir Word şablonunda. Her adımda bağlam yeniden kurulur, saatler kaybolur.</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:32px 0 32px 28px">
|
||||||
|
<span style="font-family:var(--font-heading);font-size:15px;color:var(--color-accent-700);font-feature-settings:'tnum' 1">02 / AyrisLegal ile</span>
|
||||||
|
<p style="font-size:15.5px;line-height:1.7;margin:14px 0 0;text-align:justify;hyphens:auto;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Tek bir çalışma alanı: dosyanızı yükleyin, AyrisLegal ile tartışın, içtihat bulun, dilekçe taslağı üretin — hepsi aynı ekranda, bağlamı hiç kaybetmeden.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="hr" style="margin:0;max-width:1240px;margin-inline:auto">
|
||||||
|
|
||||||
|
<section id="farklilasim" data-reveal="farklilasim" style="max-width:1240px;margin:0 auto;padding:clamp(56px,9vw,108px) clamp(20px,6vw,72px);opacity:{{ reveal.farklilasim.opacity }};transform:{{ reveal.farklilasim.transform }};transition:opacity .7s ease,transform .7s ease">
|
||||||
|
<span style="display:block;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent-700);font-feature-settings:'tnum' 1;margin-bottom:14px">Madde 03 — Tartışma odaklı yapay zeka</span>
|
||||||
|
<div style="display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:56px;align-items:center">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(28px,3.2vw,40px);margin:0 0 20px;letter-spacing:-0.008em;max-width:16ch">Bulmakla yetinmez, sizinle tartışır.</h2>
|
||||||
|
<p style="font-size:15.5px;line-height:1.7;margin:0 0 16px;max-width:46ch;text-align:justify;hyphens:auto;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Çoğu hukuk yazılımı özet çıkarır ya da anahtar kelimeyle arar. AyrisLegal dosyanızdaki argümanı okur, zayıf noktasını sorar, karşı görüş üretir, sizi ikna etmeye çalışır — ya da ikna olur.</p>
|
||||||
|
<p style="font-size:13px;margin:0;color:color-mix(in srgb, var(--color-text) 58%, transparent)">Aşağıdaki görsel, bu tartışmanın nasıl göründüğünü gösteren temsili bir tasarımdır.</p>
|
||||||
|
</div>
|
||||||
|
<div class="plate" style="margin:0;padding:28px;display:flex;flex-direction:column;gap:14px">
|
||||||
|
<div style="align-self:flex-end;max-width:78%;background:var(--color-bg);border:1px solid var(--color-divider);border-radius:var(--radius-md) var(--radius-md) 4px var(--radius-md);padding:14px 16px">
|
||||||
|
<div style="font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin-bottom:6px">Avukat</div>
|
||||||
|
<p style="margin:0;font-size:14.5px;line-height:1.6">Müvekkilin haklı olduğunu düşünüyorum, mülkiyet devri geçerli.</p>
|
||||||
|
</div>
|
||||||
|
<div style="align-self:flex-start;max-width:82%;background:var(--color-surface);border:1px solid var(--color-accent);border-radius:var(--radius-md) var(--radius-md) var(--radius-md) 4px;padding:14px 16px">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||||
|
<span style="font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-accent-700)">AyrisLegal</span>
|
||||||
|
<span class="tag tag-outline" style="font-size:10.5px">İtiraz</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:0;font-size:14.5px;line-height:1.6">Devir tarihinde tapuda şerh var — bu, iyi niyet iddianızı zayıflatabilir. Şerhin kaldırılış tarihini kontrol ettiniz mi?</p>
|
||||||
|
</div>
|
||||||
|
<div style="align-self:flex-end;max-width:78%;background:var(--color-bg);border:1px solid var(--color-divider);border-radius:var(--radius-md) var(--radius-md) 4px var(--radius-md);padding:14px 16px">
|
||||||
|
<p style="margin:0;font-size:14.5px;line-height:1.6">Haklısınız — 3 emsal kararla bu noktayı destekleyelim.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="hr" style="margin:0;max-width:1240px;margin-inline:auto">
|
||||||
|
|
||||||
|
<section id="ozellikler" data-reveal="ozellikler" style="max-width:1240px;margin:0 auto;padding:clamp(56px,9vw,108px) clamp(20px,6vw,72px);opacity:{{ reveal.ozellikler.opacity }};transform:{{ reveal.ozellikler.transform }};transition:opacity .7s ease,transform .7s ease">
|
||||||
|
<span style="display:block;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent-700);font-feature-settings:'tnum' 1;margin-bottom:14px">Madde 04 — Platform</span>
|
||||||
|
<h2 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(30px,3.6vw,44px);margin:0 0 40px;letter-spacing:-0.008em">Bir avukatın ihtiyaç duyduğu şeyler.</h2>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:20px">
|
||||||
|
<div class="card elev-sm" style="grid-column:span 2;display:flex;flex-direction:column;gap:10px;justify-content:center">
|
||||||
|
<span class="card-kicker">AI tartışma ortağı</span>
|
||||||
|
<h3 class="card-title" style="font-size:26px">Argümanınızı sınar, meslektaş gibi</h3>
|
||||||
|
<p class="card-body">Zayıf noktaları sorar, karşı görüş üretir, sizi zorlar — pasif özet değil, aktif muhakeme. AyrisLegal'ın temel farkı budur.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card elev-sm">
|
||||||
|
<span class="card-kicker">Dosya analizi</span>
|
||||||
|
<h3 class="card-title">Dava dosyasını okur</h3>
|
||||||
|
<p class="card-body">Olay örgüsünü, tarafları ve hukuki dayanakları çıkarır.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card elev-sm">
|
||||||
|
<span class="card-kicker">İçtihat arama</span>
|
||||||
|
<h3 class="card-title">Bağlama uygun emsal bulur</h3>
|
||||||
|
<p class="card-body">Anahtar kelime eşleşmesiyle değil, hukuki benzerlikle emsal kararları bulur.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card elev-sm">
|
||||||
|
<span class="card-kicker">Dilekçe taslağı</span>
|
||||||
|
<h3 class="card-title">Tartışmayı taslağa döker</h3>
|
||||||
|
<p class="card-body">Vardığınız sonucu, düzenlenebilir bir dilekçe taslağına dönüştürür.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card elev-sm">
|
||||||
|
<span class="card-kicker" style="display:flex;align-items:center;gap:8px">Büro paylaşımı <span class="tag tag-outline">Yakında</span></span>
|
||||||
|
<h3 class="card-title">Ekip çalışması</h3>
|
||||||
|
<p class="card-body">Dosya ve tartışma geçmişini büro içindeki meslektaşlarınızla paylaşın.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="hr" style="margin:0;max-width:1240px;margin-inline:auto">
|
||||||
|
|
||||||
|
<section id="fiyatlandirma" data-reveal="fiyatlandirma" style="max-width:1240px;margin:0 auto;padding:clamp(56px,9vw,108px) clamp(20px,6vw,72px);opacity:{{ reveal.fiyatlandirma.opacity }};transform:{{ reveal.fiyatlandirma.transform }};transition:opacity .7s ease,transform .7s ease">
|
||||||
|
<span style="display:block;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent-700);font-feature-settings:'tnum' 1;margin-bottom:14px;text-align:center">Madde 05 — Fiyatlandırma</span>
|
||||||
|
<h2 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(30px,3.6vw,44px);margin:0 0 44px;text-align:center;letter-spacing:-0.008em">Tek fiyat, herkese aynı.</h2>
|
||||||
|
<div style="max-width:520px;margin:0 auto;display:flex;border:1px solid var(--color-divider);border-radius:var(--radius-lg);box-shadow:var(--shadow-md);overflow:hidden">
|
||||||
|
<div style="width:34px;background:var(--color-neutral-100);display:flex;flex-direction:column;align-items:center;justify-content:space-evenly;border-right:1px solid var(--color-divider);padding:20px 0">
|
||||||
|
<sc-for list="{{ pricingTicks }}" as="t" hint-placeholder-count="5">
|
||||||
|
<span style="font-size:11px;font-feature-settings:'tnum' 1;color:var(--color-accent-700);writing-mode:vertical-rl">{{ t }}</span>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;padding:40px 36px;text-align:center">
|
||||||
|
<p style="margin:0;font-family:var(--font-heading);font-weight:400;font-size:clamp(46px,6vw,64px);line-height:1;font-feature-settings:'tnum' 1">40.000 TL</p>
|
||||||
|
<p style="margin:6px 0 0;font-size:14px;letter-spacing:0.06em;text-transform:uppercase;color:color-mix(in srgb, var(--color-text) 60%, transparent)">/ yıl</p>
|
||||||
|
<p style="font-size:15px;line-height:1.6;max-width:32ch;margin:20px auto 0;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Bireysel çalışan avukat ya da büro ortağı — fark etmez. Tüm bireysel avukatlara tek, sabit fiyat.</p>
|
||||||
|
<hr class="hr" style="margin:24px 0">
|
||||||
|
<ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px;text-align:left">
|
||||||
|
<li style="display:flex;gap:10px;font-size:14.5px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex:none;margin-top:2px"><polyline points="20 6 9 17 4 12"></polyline></svg>Sınırsız dosya analizi</li>
|
||||||
|
<li style="display:flex;gap:10px;font-size:14.5px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex:none;margin-top:2px"><polyline points="20 6 9 17 4 12"></polyline></svg>AI tartışma ortağı</li>
|
||||||
|
<li style="display:flex;gap:10px;font-size:14.5px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex:none;margin-top:2px"><polyline points="20 6 9 17 4 12"></polyline></svg>İçtihat arama</li>
|
||||||
|
<li style="display:flex;gap:10px;font-size:14.5px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex:none;margin-top:2px"><polyline points="20 6 9 17 4 12"></polyline></svg>Dilekçe taslağı üretimi</li>
|
||||||
|
<li style="display:flex;gap:10px;font-size:14.5px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex:none;margin-top:2px"><polyline points="20 6 9 17 4 12"></polyline></svg>Kurucu ekiple doğrudan destek</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#iletisim" class="btn btn-primary btn-block" style="margin-top:26px;font-size:15px;padding:12px">Demo Talep Et</a>
|
||||||
|
<p style="font-size:12px;margin:14px 0 0;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Yıllık faturalandırma. Gizli ücret yok.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="hr" style="margin:0;max-width:1240px;margin-inline:auto">
|
||||||
|
|
||||||
|
<section id="guven" data-reveal="guven" style="max-width:1240px;margin:0 auto;padding:clamp(56px,9vw,108px) clamp(20px,6vw,72px);opacity:{{ reveal.guven.opacity }};transform:{{ reveal.guven.transform }};transition:opacity .7s ease,transform .7s ease">
|
||||||
|
<span style="display:block;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent-700);font-feature-settings:'tnum' 1;margin-bottom:14px">Madde 06 — Güven ve veri güvenliği</span>
|
||||||
|
<h2 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(30px,3.6vw,44px);margin:0 0 16px;letter-spacing:-0.008em">Dosyalarınız sizin kalır.</h2>
|
||||||
|
<p style="font-size:15.5px;line-height:1.7;max-width:60ch;margin:0 0 40px;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Bir dava dosyası, bir avukatın elindeki en hassas belgedir. AyrisLegal'ı bu bilinçle kuruyoruz.</p>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:0;border-top:1px solid var(--color-divider)">
|
||||||
|
<div style="padding:28px 24px 0 0;border-right:1px solid var(--color-divider)">
|
||||||
|
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 L20 5 V11 C20 16 16.5 19.5 12 21 C7.5 19.5 4 16 4 11 V5 Z"></path><path d="M9 12 l2.2 2.2 L15.5 9.5"></path></svg>
|
||||||
|
<h3 style="font-family:var(--font-heading);font-weight:400;font-size:19px;margin:12px 0 6px">KVKK'ya uyumlu</h3>
|
||||||
|
<p style="font-size:14.5px;line-height:1.6;margin:0;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Kişisel verilerin korunması mevzuatına uygun işleme ve saklama.</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:28px 24px 0;border-right:1px solid var(--color-divider)">
|
||||||
|
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="6" rx="1.5"></rect><rect x="3" y="14" width="18" height="6" rx="1.5"></rect><line x1="7" y1="7" x2="7" y2="7"></line><line x1="7" y1="17" x2="7" y2="17"></line></svg>
|
||||||
|
<h3 style="font-family:var(--font-heading);font-weight:400;font-size:19px;margin:12px 0 6px">Sunucular Türkiye'de</h3>
|
||||||
|
<p style="font-size:14.5px;line-height:1.6;margin:0;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Verileriniz yurt dışına çıkmaz — Ayris Tech'in self-hosted altyapı felsefesiyle.</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:28px 0 0 24px">
|
||||||
|
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="10" width="14" height="10" rx="1.5"></rect><path d="M8 10 V7 a4 4 0 0 1 8 0 v3"></path></svg>
|
||||||
|
<h3 style="font-family:var(--font-heading);font-weight:400;font-size:19px;margin:12px 0 6px">Dosyanız modelleri eğitmez</h3>
|
||||||
|
<p style="font-size:14.5px;line-height:1.6;margin:0;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Yüklediğiniz belgeler yalnızca sizin analizinizde kullanılır.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="hr" style="margin:0;max-width:1240px;margin-inline:auto">
|
||||||
|
|
||||||
|
<section id="iletisim" data-reveal="iletisim" style="max-width:1240px;margin:0 auto;padding:clamp(56px,9vw,108px) clamp(20px,6vw,72px);opacity:{{ reveal.iletisim.opacity }};transform:{{ reveal.iletisim.transform }};transition:opacity .7s ease,transform .7s ease">
|
||||||
|
<span style="display:block;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--color-accent-700);font-feature-settings:'tnum' 1;margin-bottom:14px">Madde 07 — Erken erişim</span>
|
||||||
|
<div style="display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:48px">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-family:var(--font-heading);font-weight:400;font-size:clamp(28px,3.2vw,38px);margin:0 0 16px;letter-spacing:-0.008em">Şu an müşteri referansı göstermiyoruz — kasıtlı olarak.</h2>
|
||||||
|
<p style="font-size:15.5px;line-height:1.7;margin:0;text-align:justify;hyphens:auto;color:color-mix(in srgb, var(--color-text) 78%, transparent)">AyrisLegal yeni. Sahte ya da ödünç yorumlar yerine dürüst olmayı seçtik: platformu kurucu ekiple birlikte, doğrudan geri bildiriminizle şekillendiriyoruz. Demo talep edin, ilk kullanan avukatlardan biri olun.</p>
|
||||||
|
<div style="margin-top:32px;display:flex;align-items:center;gap:14px">
|
||||||
|
<div style="width:44px;height:44px;border:1px solid var(--color-accent);border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:var(--font-heading);font-size:11px;letter-spacing:0.03em;color:var(--color-accent-700);text-align:center;line-height:1.1">AYRIS<br/>LEGAL</div>
|
||||||
|
<span style="font-size:12.5px;color:color-mix(in srgb, var(--color-text) 60%, transparent)">Kurucu ekip mührü</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<sc-if value="{{ submitted }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="border:1px solid var(--color-divider);border-radius:var(--radius-lg);padding:40px;text-align:center;background:var(--color-surface)">
|
||||||
|
<h3 style="font-family:var(--font-heading);font-weight:400;font-size:22px;margin:0 0 8px">Talebiniz alındı.</h3>
|
||||||
|
<p style="font-size:14.5px;margin:0;color:color-mix(in srgb, var(--color-text) 78%, transparent)">Kurucu ekipten kısa süre içinde size dönüş yapılacak.</p>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
<sc-if value="{{ notSubmitted }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<form onSubmit="{{ handleSubmit }}" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;border:1px solid var(--color-divider);border-radius:var(--radius-lg);padding:28px">
|
||||||
|
<div class="field" style="grid-column:span 1">
|
||||||
|
<label for="af-name">Ad Soyad</label>
|
||||||
|
<input id="af-name" class="input" type="text" placeholder="Av. Ayşe Yılmaz" required="{{ true }}"></input>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="af-org">Büro / Kurum</label>
|
||||||
|
<input id="af-org" class="input" type="text" placeholder="Yılmaz Hukuk Bürosu"></input>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="af-email">E-posta</label>
|
||||||
|
<input id="af-email" class="input" type="email" placeholder="ayse@buro.com" required="{{ true }}"></input>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="af-phone">Telefon</label>
|
||||||
|
<input id="af-phone" class="input" type="tel" placeholder="0532 000 00 00"></input>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block" style="grid-column:1 / -1;padding:12px">Demo Talep Et</button>
|
||||||
|
</form>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer style="max-width:1240px;margin:0 auto;padding:28px clamp(20px,6vw,72px) 40px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px;font-size:13px;color:color-mix(in srgb, var(--color-text) 60%, transparent)">
|
||||||
|
<span>AyrisLegal, Ayris Tech'in bir ürünüdür.</span>
|
||||||
|
<span>© {{ year }} AyrisLegal · <a href="#" style="color:inherit">Aydınlatma Metni</a></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</x-dc>
|
||||||
|
<script type="text/x-dc" data-dc-script>
|
||||||
|
const SECTIONS = ['problem','farklilasim','ozellikler','fiyatlandirma','guven','iletisim'];
|
||||||
|
const RAIL = [
|
||||||
|
{ id: 'problem', label: '02', href: '#is-akisi' },
|
||||||
|
{ id: 'farklilasim', label: '03', href: '#farklilasim' },
|
||||||
|
{ id: 'ozellikler', label: '04', href: '#ozellikler' },
|
||||||
|
{ id: 'fiyatlandirma', label: '05', href: '#fiyatlandirma' },
|
||||||
|
{ id: 'guven', label: '06', href: '#guven' },
|
||||||
|
{ id: 'iletisim', label: '07', href: '#iletisim' },
|
||||||
|
];
|
||||||
|
|
||||||
|
class Component extends DCLogic {
|
||||||
|
state = { submitted: false, visible: {}, scrolled: false, active: null };
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._onScroll = () => this.setState({ scrolled: window.scrollY > 12 });
|
||||||
|
window.addEventListener('scroll', this._onScroll, { passive: true });
|
||||||
|
this._observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
const id = e.target.getAttribute('data-reveal');
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
this.setState((s) => ({ visible: { ...s.visible, [id]: true }, active: id }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.2 });
|
||||||
|
document.querySelectorAll('[data-reveal]').forEach((el) => this._observer.observe(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('scroll', this._onScroll);
|
||||||
|
if (this._observer) this._observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({ submitted: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
renderVals() {
|
||||||
|
const reveal = {};
|
||||||
|
SECTIONS.forEach((id) => {
|
||||||
|
const on = !!this.state.visible[id];
|
||||||
|
reveal[id] = { opacity: on ? 1 : 0, transform: on ? 'translateY(0)' : 'translateY(28px)' };
|
||||||
|
});
|
||||||
|
const railItems = RAIL.map((r) => ({
|
||||||
|
...r,
|
||||||
|
color: this.state.active === r.id ? 'var(--color-accent-700)' : 'color-mix(in srgb, var(--color-text) 40%, transparent)',
|
||||||
|
weight: this.state.active === r.id ? 600 : 400,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
submitted: this.state.submitted,
|
||||||
|
notSubmitted: !this.state.submitted,
|
||||||
|
handleSubmit: this.handleSubmit,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
reveal,
|
||||||
|
railItems,
|
||||||
|
navPad: this.state.scrolled ? '10px clamp(20px,6vw,64px)' : '14px clamp(20px,6vw,64px)',
|
||||||
|
navBg: this.state.scrolled ? 'color-mix(in srgb, var(--color-bg) 88%, transparent)' : 'transparent',
|
||||||
|
stats: [
|
||||||
|
{ value: '01', label: 'Sabit fiyat, tüm avukatlara' },
|
||||||
|
{ value: '40.000 TL', label: 'Yıllık, gizli ücret yok' },
|
||||||
|
{ value: '%100', label: 'Veri Türkiye\u2019de kalır' },
|
||||||
|
{ value: '7/24', label: 'Dosyanızla çalışan ortak' },
|
||||||
|
],
|
||||||
|
pricingTicks: ['01', '02', '03', '04', '05'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"import"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/forbid-elements": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"forbid": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"patterns": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-syntax": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]",
|
||||||
|
"message": "Raw hex color — use a design-system color token via var()."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "Literal[value=/\\b\\d+px\\b/]",
|
||||||
|
"message": "Raw px value — use a design-system spacing token via var()."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:Cormorant Garamond|Lora))/i]",
|
||||||
|
"message": "Font not provided by the design system. Available: Cormorant Garamond, Lora."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/index.js"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-restricted-imports": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-omelette": {
|
||||||
|
"components": {},
|
||||||
|
"tokens": [
|
||||||
|
"--color-accent",
|
||||||
|
"--color-accent-100",
|
||||||
|
"--color-accent-2",
|
||||||
|
"--color-accent-2-100",
|
||||||
|
"--color-accent-2-200",
|
||||||
|
"--color-accent-2-300",
|
||||||
|
"--color-accent-2-400",
|
||||||
|
"--color-accent-2-500",
|
||||||
|
"--color-accent-2-600",
|
||||||
|
"--color-accent-2-700",
|
||||||
|
"--color-accent-2-800",
|
||||||
|
"--color-accent-2-900",
|
||||||
|
"--color-accent-200",
|
||||||
|
"--color-accent-300",
|
||||||
|
"--color-accent-400",
|
||||||
|
"--color-accent-500",
|
||||||
|
"--color-accent-600",
|
||||||
|
"--color-accent-700",
|
||||||
|
"--color-accent-800",
|
||||||
|
"--color-accent-900",
|
||||||
|
"--color-bg",
|
||||||
|
"--color-divider",
|
||||||
|
"--color-neutral-100",
|
||||||
|
"--color-neutral-200",
|
||||||
|
"--color-neutral-300",
|
||||||
|
"--color-neutral-400",
|
||||||
|
"--color-neutral-500",
|
||||||
|
"--color-neutral-600",
|
||||||
|
"--color-neutral-700",
|
||||||
|
"--color-neutral-800",
|
||||||
|
"--color-neutral-900",
|
||||||
|
"--color-surface",
|
||||||
|
"--color-text",
|
||||||
|
"--font-body",
|
||||||
|
"--font-heading",
|
||||||
|
"--font-heading-weight",
|
||||||
|
"--radius-lg",
|
||||||
|
"--radius-md",
|
||||||
|
"--radius-sm",
|
||||||
|
"--shadow-lg",
|
||||||
|
"--shadow-md",
|
||||||
|
"--shadow-sm",
|
||||||
|
"--space-1",
|
||||||
|
"--space-2",
|
||||||
|
"--space-3",
|
||||||
|
"--space-4",
|
||||||
|
"--space-6",
|
||||||
|
"--space-8"
|
||||||
|
],
|
||||||
|
"tokenKinds": {
|
||||||
|
"--color-bg": "color",
|
||||||
|
"--color-surface": "color",
|
||||||
|
"--color-text": "font",
|
||||||
|
"--color-accent": "color",
|
||||||
|
"--color-accent-2": "color",
|
||||||
|
"--color-divider": "color",
|
||||||
|
"--color-neutral-100": "color",
|
||||||
|
"--color-neutral-200": "color",
|
||||||
|
"--color-neutral-300": "color",
|
||||||
|
"--color-neutral-400": "color",
|
||||||
|
"--color-neutral-500": "color",
|
||||||
|
"--color-neutral-600": "color",
|
||||||
|
"--color-neutral-700": "color",
|
||||||
|
"--color-neutral-800": "color",
|
||||||
|
"--color-neutral-900": "color",
|
||||||
|
"--color-accent-100": "color",
|
||||||
|
"--color-accent-200": "color",
|
||||||
|
"--color-accent-300": "color",
|
||||||
|
"--color-accent-400": "color",
|
||||||
|
"--color-accent-500": "color",
|
||||||
|
"--color-accent-600": "color",
|
||||||
|
"--color-accent-700": "color",
|
||||||
|
"--color-accent-800": "color",
|
||||||
|
"--color-accent-900": "color",
|
||||||
|
"--color-accent-2-100": "color",
|
||||||
|
"--color-accent-2-200": "color",
|
||||||
|
"--color-accent-2-300": "color",
|
||||||
|
"--color-accent-2-400": "color",
|
||||||
|
"--color-accent-2-500": "color",
|
||||||
|
"--color-accent-2-600": "color",
|
||||||
|
"--color-accent-2-700": "color",
|
||||||
|
"--color-accent-2-800": "color",
|
||||||
|
"--color-accent-2-900": "color",
|
||||||
|
"--font-heading": "font",
|
||||||
|
"--font-heading-weight": "font",
|
||||||
|
"--font-body": "font",
|
||||||
|
"--space-1": "spacing",
|
||||||
|
"--space-2": "spacing",
|
||||||
|
"--space-3": "spacing",
|
||||||
|
"--space-4": "spacing",
|
||||||
|
"--space-6": "spacing",
|
||||||
|
"--space-8": "spacing",
|
||||||
|
"--radius-sm": "radius",
|
||||||
|
"--radius-md": "radius",
|
||||||
|
"--radius-lg": "radius",
|
||||||
|
"--shadow-sm": "shadow",
|
||||||
|
"--shadow-md": "shadow",
|
||||||
|
"--shadow-lg": "shadow"
|
||||||
|
},
|
||||||
|
"fontFamilies": [
|
||||||
|
"Cormorant Garamond",
|
||||||
|
"Lora"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* @ds-bundle: {"format":4,"namespace":"Classical_classi","components":[],"sourceHashes":{},"inlinedExternals":[],"unexposedExports":[]} */
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const __ds_ns = (window.Classical_classi = window.Classical_classi || {});
|
||||||
|
|
||||||
|
const __ds_scope = {};
|
||||||
|
|
||||||
|
(__ds_ns.__errors = __ds_ns.__errors || []);
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Classical design system
|
||||||
|
|
||||||
|
Classical is an editorial, book-like system on a soft near-white ground: Cormorant Garamond headings over Lora body, justified columns, hairline rules, and color applied as stroke rather than fill. Surfaces stay quiet — cards are bordered, buttons are outlined — and photographs sit matted on the page like tipped-in plates.
|
||||||
|
|
||||||
|
## How to use this
|
||||||
|
|
||||||
|
- Link the one stylesheet from every page — `<link rel="stylesheet" href="styles.css">` (adjust the relative path) — and take every color, font, spacing, radius and shadow from its variables (`var(--color-*)`, `var(--font-*)`, `var(--space-*)`, `var(--radius-*)`, `var(--shadow-*)`). Never hard-code a hex, a font name or a px value the tokens already carry.
|
||||||
|
- Build with the classes below rather than inventing parallel ones; the component pages are plain HTML, so view source and copy the markup.
|
||||||
|
- `templates/` holds starting points a consuming project can copy whole.
|
||||||
|
- The whole system was derived from `theme.json`. To change the look, edit the tokens at the top of `styles.css` — every page, the thumbnail and this guide read from them — and keep `theme.json` and the written guidance in step so they don't drift from what the CSS actually does.
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
Editorial, justified body copy in columns. Tight leading; headlines flush-left over justified text blocks. Use hairline dividers (`var(--color-divider)`) between major sections. Apply color as borders, rules and underlines — not as filled blocks. Buttons are outlined (1px accent border on transparent), not solid-filled. Wrap hero and inline images in the `.plate` class — a warm archival grade inside a thin surface-colored mat, like a tipped-in book plate. In decks, section dividers sit on a deep warm near-black (a shade below `--color-neutral-900`) as a colophon page — paper type with a gold ghost numeral; gold stays stroke and small marks on content slides.
|
||||||
|
|
||||||
|
## Color
|
||||||
|
|
||||||
|
A light ground (`--color-bg` #f3f2f2) with `--color-text` #201f1d and a single accent #b68235 (this is a mono scheme: no second accent was chosen — the `--color-accent-2-*` variables carry a machine-derived stand-in kept only so both sets resolve; treat them as one role). Each role carries a 100–900 tonal ramp (`--color-neutral-100` … `--color-accent-2-900`) generated in OKLCH on a shared perceptual lightness scale, so the same step of any ramp has the same visual weight. Use the light steps (100–300) for tinted fills, hovers and subtle borders, 500 as the role's base, and the dark steps (700–900) for text on tinted fills and for pressed states; prefer ramp steps over ad-hoc `color-mix()`. For elevation use `--shadow-sm/md/lg` (already tuned to the ground) rather than ad-hoc box-shadows.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
Cormorant Garamond for headings over Lora for body text, loaded as `--font-heading` / `--font-body`. Bold is avoided: interface headings cap at semibold (the `--font-heading-weight` token), and the bigger the text the lighter it sets — display sizes take the normal cut (the deck shows this). Numbers set tabular wherever they stand as figures or columns — kickers, contents numbers, tables, charts, the display numerals (`"tnum"`; both faces keep their own figure style and gain equal widths) — while running prose keeps its text figures (Lora's tabular feature also widens its word-spaces and punctuation, which would loosen the prose). Density 1.15× and radius 4px are already baked into the `--space-*` / `--radius-*` scales — use the variables, not raw numbers.
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
Use Lucide icons (https://lucide.dev) throughout.
|
||||||
|
|
||||||
|
## Interaction states
|
||||||
|
|
||||||
|
Interactive states are themed, never browser defaults: give every interactive element a `:hover` tint and a pressed state from the accent ramp (one step past the base — `--color-accent-600` on a light ground, `--color-accent-400` on a dark one, or a `color-mix()` tint for outlined/ghost variants), and style keyboard focus with `:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }` — never leave the default blue focus ring.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
| Class | What it is | Shown in |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `.btn` with `.btn-primary`, `.btn-secondary`, `.btn-ghost`, `.btn-icon`, `.btn-block` | Actions — the primary is an accent outline, never a fill | components/buttons.html |
|
||||||
|
| `.tag` with `.tag-accent`, `.tag-accent-2`, `.tag-neutral`, `.tag-outline` | Small labels tinted from the ramps (mono palette: accent-2 reads the same as accent) | components/buttons.html |
|
||||||
|
| `.field` + `label`, `.input`, `.radio` + `.dot`, `.seg` + `.seg-opt` | Form fields and choices on native elements — no script | components/forms.html |
|
||||||
|
| `.card` with `.card-kicker`, `.card-title`, `.card-body`, `.card-meta`; `.elev-sm/md/lg` | Bordered, unfilled content surfaces; elevation utilities | components/cards.html |
|
||||||
|
| `.nav` + `.nav-brand` | The header bar | components/navigation.html |
|
||||||
|
| `.table` | Data tables with themed header and row rules | components/table.html |
|
||||||
|
| `.dialog-backdrop` + `.dialog` (+ `.dialog-title/-body/-actions`) | A modal at the top elevation | components/dialog.html |
|
||||||
|
| `.hr` | A hairline horizontal rule | foundations/layout.html |
|
||||||
|
| `.plate` | The image wrapper — every content photograph goes through it | foundations/image.html |
|
||||||
|
|
||||||
|
States are built in: hovers and pressed states come from the accent ramp, keyboard focus is the 2px accent `:focus-visible` ring, `::selection` is an accent tint, and disabled controls drop to 45% opacity. Don't restyle them per page. The accent-to-ground pair is tuned to at least 3:1 — enough for icons, large text and interface chrome, not for body copy — so for paragraph-size text in the accent use a deep ramp step (`--color-accent-700` on this ground) rather than the accent itself.
|
||||||
|
|
||||||
|
## Do
|
||||||
|
|
||||||
|
- Justify body copy at a comfortable measure and let the hairlines carry the structure.
|
||||||
|
- Draw with borders, rules and underlines; keep large fills off the page.
|
||||||
|
- Give text room — the spacing scale is airy (density 1.15×) by design.
|
||||||
|
- Mat photographs with the `.plate` wrapper so they read as plates, not banners.
|
||||||
|
|
||||||
|
## Don't
|
||||||
|
|
||||||
|
- Do not fill cards or buttons with solid accent color.
|
||||||
|
- Do not use heavy drop shadows — elevation here is a whisper.
|
||||||
|
- Do not tighten the leading or crowd the margins.
|
||||||
|
- Do not swap in a sans-serif for emphasis; weight and italics do that job.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `styles.css` — the only stylesheet: the token sheet (`:root` variables, ramps, base type) plus the component layer. Link it from every page.
|
||||||
|
- `readme.md` — this guide.
|
||||||
|
- `theme.json` — the parameters these files were derived from (a machine-readable record of the theme).
|
||||||
|
- `thumbnail.html` — the project cover (brand mark + swatches).
|
||||||
|
- `foundations/type.html` — the type scale and the heading/body pairing at real sizes.
|
||||||
|
- `foundations/color.html` — color roles and the 100-900 tonal ramps, with usage notes.
|
||||||
|
- `foundations/layout.html` — the spacing scale, the grid and how edges are drawn.
|
||||||
|
- `foundations/icons.html` — the icon set at interface sizes, inline and in buttons.
|
||||||
|
- `foundations/image.html` — how photographs and figures are treated.
|
||||||
|
- `components/buttons.html` — buttons, icon buttons and tags in every variant and state.
|
||||||
|
- `components/forms.html` — text fields, radios and the segmented control on native elements.
|
||||||
|
- `components/cards.html` — content cards and the elevation steps.
|
||||||
|
- `components/navigation.html` — the header bar pattern.
|
||||||
|
- `components/table.html` — a data table with the themed header and row rules.
|
||||||
|
- `components/dialog.html` — a modal over its backdrop at the top elevation.
|
||||||
|
- `theme.html` — the theme's parameters rendered as a reference sheet.
|
||||||
|
- `templates/landing/` — a starter page consuming the system the intended way (`index.html`, its `ds-base.js` loader, and the vendored `image-slot.js` its photograph mounts).
|
||||||
|
- `assets/photo.jpg` — the reference photograph the imagery page treats.
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600&family=Lora:wght@400;600&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-bg: #f3f2f2;
|
||||||
|
--color-surface: #eae9e9;
|
||||||
|
--color-text: #201f1d;
|
||||||
|
--color-accent: #b68235;
|
||||||
|
--color-accent-2: #ac803e;
|
||||||
|
--color-divider: color-mix(in srgb, #201f1d 16%, transparent);
|
||||||
|
|
||||||
|
/* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
|
||||||
|
same step of any role matches the others in visual value. */
|
||||||
|
--color-neutral-100: #f8f4f4;
|
||||||
|
--color-neutral-200: #eae7e7;
|
||||||
|
--color-neutral-300: #d7d3d3;
|
||||||
|
--color-neutral-400: #bab6b6;
|
||||||
|
--color-neutral-500: #9b9797;
|
||||||
|
--color-neutral-600: #7d7979;
|
||||||
|
--color-neutral-700: #605d5d;
|
||||||
|
--color-neutral-800: #444141;
|
||||||
|
--color-neutral-900: #2d2b2b;
|
||||||
|
|
||||||
|
--color-accent-100: #fff3e4;
|
||||||
|
--color-accent-200: #ffe3bf;
|
||||||
|
--color-accent-300: #facb8d;
|
||||||
|
--color-accent-400: #e1ad66;
|
||||||
|
--color-accent-500: #c28d41;
|
||||||
|
--color-accent-600: #a06f24;
|
||||||
|
--color-accent-700: #7d5411;
|
||||||
|
--color-accent-800: #5a3b0a;
|
||||||
|
--color-accent-900: #3a270d;
|
||||||
|
|
||||||
|
--color-accent-2-100: #fff3e4;
|
||||||
|
--color-accent-2-200: #ffe3be;
|
||||||
|
--color-accent-2-300: #f5cd96;
|
||||||
|
--color-accent-2-400: #dbaf70;
|
||||||
|
--color-accent-2-500: #bc8f4e;
|
||||||
|
--color-accent-2-600: #9b7232;
|
||||||
|
--color-accent-2-700: #79561f;
|
||||||
|
--color-accent-2-800: #573d14;
|
||||||
|
--color-accent-2-900: #382810;
|
||||||
|
|
||||||
|
--font-heading: "Cormorant Garamond", system-ui, sans-serif;
|
||||||
|
--font-heading-weight: 600; /* review round 3: bold retired — semibold is the
|
||||||
|
interface headings' ceiling (they need the weight at small sizes); display
|
||||||
|
text goes lighter still, to the normal cut (see the deck) */
|
||||||
|
--font-body: "Lora", system-ui, sans-serif;
|
||||||
|
|
||||||
|
--space-1: 4.6px;
|
||||||
|
--space-2: 9.2px;
|
||||||
|
--space-3: 13.8px;
|
||||||
|
--space-4: 18.4px;
|
||||||
|
--space-6: 27.6px;
|
||||||
|
--space-8: 36.8px;
|
||||||
|
|
||||||
|
--radius-sm: 2px;
|
||||||
|
--radius-md: 4px;
|
||||||
|
--radius-lg: 7px;
|
||||||
|
|
||||||
|
/* Elevation — derived from the ground: soft ink-tinted shadows on a
|
||||||
|
light theme, a hairline edge + ambient darkness on a dark one. */
|
||||||
|
--shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);
|
||||||
|
--shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);
|
||||||
|
--shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }
|
||||||
|
|
||||||
|
.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;
|
||||||
|
border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════════════
|
||||||
|
Components — built with the tokens above. Plain CSS
|
||||||
|
on plain HTML: no JavaScript, no build step. Each class is documented in
|
||||||
|
readme.md and demonstrated in foundations/ and components/.
|
||||||
|
══════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||||
|
line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
|
||||||
|
}
|
||||||
|
h1 { font-size: 42px; }
|
||||||
|
h2 { font-size: 32px; }
|
||||||
|
h3 { font-size: 25px; }
|
||||||
|
h4 { font-size: 20px; }
|
||||||
|
h5 { font-size: 16px; }
|
||||||
|
h6 { font-size: 13px; }
|
||||||
|
h6 { letter-spacing: 0.08em; text-transform: uppercase; }
|
||||||
|
p { margin: 0 0 var(--space-3); }
|
||||||
|
a { color: var(--color-accent); text-underline-offset: 3px; }
|
||||||
|
img { display: block; max-width: 100%; }
|
||||||
|
figure { margin: 0; }
|
||||||
|
figcaption {
|
||||||
|
font-size: 11px; margin-top: var(--space-1);
|
||||||
|
color: color-mix(in srgb, var(--color-text) 55%, transparent);
|
||||||
|
}
|
||||||
|
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
|
||||||
|
:focus { outline: none; }
|
||||||
|
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
||||||
|
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }
|
||||||
|
|
||||||
|
/* — rules — */
|
||||||
|
.hr {
|
||||||
|
height: 1px; border: 0; margin: var(--space-4) 0;
|
||||||
|
background: var(--color-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* — buttons — */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||||
|
cursor: pointer; text-decoration: none;
|
||||||
|
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||||
|
font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —
|
||||||
|
the pair sits side by side in sign-up rows */
|
||||||
|
background: transparent; border: 1px solid transparent;
|
||||||
|
padding: var(--space-2) calc(var(--space-3) * 1.2);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.btn svg { display: block; }
|
||||||
|
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
|
.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
|
||||||
|
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
|
||||||
|
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
|
||||||
|
.btn-secondary { border-color: var(--color-divider); }
|
||||||
|
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||||
|
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
|
||||||
|
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
|
||||||
|
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
|
||||||
|
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
|
||||||
|
.btn-icon { width: 36px; height: 36px; padding: 0; }
|
||||||
|
.btn-block { width: 100%; margin-top: var(--space-2); }
|
||||||
|
|
||||||
|
/* — forms — */
|
||||||
|
.field > label {
|
||||||
|
display: block; font-size: 12px; margin-bottom: 5px;
|
||||||
|
color: color-mix(in srgb, var(--color-text) 70%, transparent);
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;
|
||||||
|
font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-divider); border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
|
||||||
|
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
|
||||||
|
textarea.input { min-height: 90px; resize: vertical; }
|
||||||
|
.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }
|
||||||
|
.radio input, .seg-opt input {
|
||||||
|
position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
|
||||||
|
}
|
||||||
|
.radio .dot {
|
||||||
|
width: 16px; height: 16px; flex: none; border-radius: 50%;
|
||||||
|
border: 1.5px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
.radio:hover .dot { border-color: var(--color-accent); }
|
||||||
|
.radio input:checked + .dot {
|
||||||
|
border-color: var(--color-accent); background: var(--color-accent);
|
||||||
|
box-shadow: inset 0 0 0 4px var(--color-bg);
|
||||||
|
}
|
||||||
|
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
||||||
|
.seg {
|
||||||
|
display: inline-flex; overflow: hidden;
|
||||||
|
border: 1px solid var(--color-divider); border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.seg-opt {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 7px 12px; font-size: 13px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
|
||||||
|
.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
|
||||||
|
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||||
|
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }
|
||||||
|
|
||||||
|
/* — cards — */
|
||||||
|
.card {
|
||||||
|
display: flex; flex-direction: column; gap: var(--space-2);
|
||||||
|
padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
|
||||||
|
.card-title {
|
||||||
|
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||||
|
font-size: 17px; line-height: 1.2;
|
||||||
|
}
|
||||||
|
.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }
|
||||||
|
.card-meta {
|
||||||
|
display: flex; align-items: center; gap: 6px; font-size: 11px;
|
||||||
|
color: color-mix(in srgb, var(--color-text) 50%, transparent);
|
||||||
|
}
|
||||||
|
.elev-sm { box-shadow: var(--shadow-sm); }
|
||||||
|
.elev-md { box-shadow: var(--shadow-md); }
|
||||||
|
.elev-lg { box-shadow: var(--shadow-lg); }
|
||||||
|
|
||||||
|
/* — tags — */
|
||||||
|
.tag {
|
||||||
|
display: inline-flex; align-items: center; font-size: 11px;
|
||||||
|
letter-spacing: 0.02em; padding: 3px 10px;
|
||||||
|
border-radius: calc(var(--radius-md) * 0.75);
|
||||||
|
}
|
||||||
|
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
|
||||||
|
.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
|
||||||
|
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
|
||||||
|
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }
|
||||||
|
|
||||||
|
/* — navigation — */
|
||||||
|
.nav {
|
||||||
|
display: flex; align-items: center; gap: var(--space-4);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
.nav-brand {
|
||||||
|
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||||
|
font-size: 18px; margin-right: auto;
|
||||||
|
}
|
||||||
|
.nav a { color: inherit; text-decoration: none; font-size: 14px; }
|
||||||
|
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }
|
||||||
|
|
||||||
|
/* — tables — */
|
||||||
|
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||||
|
.table th {
|
||||||
|
text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
|
||||||
|
color: color-mix(in srgb, var(--color-text) 60%, transparent);
|
||||||
|
padding: var(--space-2); border-bottom: 1px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
.table td {
|
||||||
|
padding: var(--space-2);
|
||||||
|
border-bottom: 1px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
|
||||||
|
|
||||||
|
/* — dialog — */
|
||||||
|
.dialog-backdrop {
|
||||||
|
position: fixed; inset: 0; display: grid; place-items: center;
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
|
||||||
|
}
|
||||||
|
.dialog {
|
||||||
|
width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
|
||||||
|
padding: var(--space-4); border-radius: var(--radius-lg);
|
||||||
|
background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
.dialog-title {
|
||||||
|
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.dialog-body { font-size: 14px; opacity: 0.85; }
|
||||||
|
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
|
||||||
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# AyrisLegal Tanıtım Web Sitesi — Ürün Gereksinimleri Dokümanı (PRD)
|
||||||
|
|
||||||
|
**Versiyon:** v0.1 (MVP)
|
||||||
|
**Tarih:** Ağustos 2026
|
||||||
|
**Durum:** Taslak — geliştirme öncesi son inceleme bekliyor
|
||||||
|
**İlgili doküman:** AyrisLegal MVP PRD (ana uygulama — `docs/prd.md`), AyrisLegal Backend PRD (`docs/backend.md`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem Tanımı
|
||||||
|
|
||||||
|
AyrisLegal (Electron masaüstü uygulaması) geliştirilmeye devam ediyor, ancak ürünün şu anda hiçbir kamuya açık tanıtımı, markalı bir varlığı veya potansiyel kullanıcıların (avukatlar) ürünü keşfedip iletişime geçebileceği bir kanalı yok. Pilot kullanıcı toplama süreci ("eş ve meslektaş çevresi") kişisel ağ üzerinden yürüyor; bu ölçeklenebilir değil ve ürünün güvenilirlik/ciddiyet algısını (hukuk sektöründe kritik bir faktör) inşa etmiyor.
|
||||||
|
|
||||||
|
**Kim etkileniyor:** İş sahibi (Mustafa) — pilot kullanıcı ve ilk ödeyen müşterileri bulma sürecinde; potansiyel kullanıcılar (solo/küçük büro avukatları) — ürünü keşfetme, güvenilirliğini değerlendirme ve iletişime geçme kanalı olmadığı için.
|
||||||
|
|
||||||
|
**Çözülmezse maliyeti:** Ana uygulama PRD'sindeki "İlk 2 ay içinde 1.000 ödeyen kullanıcı" hedefi, ürünü keşfedecek bir kanal olmadan gerçekçi değil. Rakipler (Lawyer Team, Hukukçu.pro, SmartHukuk, KolayOfis, Attornaid) zaten kamuya açık web varlığına sahip; markasız kalmak pazar konumlandırmasında dezavantaj yaratır.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Hedefler
|
||||||
|
|
||||||
|
1. Ziyaretçinin AyrisLegal'in ne olduğunu, kime hitap ettiğini ve temel değer önerisini (pasif arama aracı değil, aktif "düşünce ortağı") 10 saniye içinde anlayabilmesi
|
||||||
|
2. Nitelikli avukat/büro ziyaretçilerinden demo talebi/iletişim formu doldurmalarını sağlamak — **sitenin birincil dönüşüm hedefi**
|
||||||
|
3. Sitenin, hukuk sektörünün beklediği güven sinyallerini (profesyonel tasarım, netlik, KVKK/gizlilik duyarlılığı) taşıması
|
||||||
|
4. Sitenin MVP uygulamayla paralel, hızlı (hedef: birkaç gün içinde) yayına alınabilmesi
|
||||||
|
5. Marka kimliğinin (AyrisLegal logosu, lacivert/teal renk paleti, Archivo tipografisi) tutarlı şekilde uygulanması
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Kapsam Dışı (Non-Goals) — MVP v0.1
|
||||||
|
|
||||||
|
| Kapsam Dışı | Neden |
|
||||||
|
|---|---|
|
||||||
|
| Self-serve kayıt/ödeme akışı (kredi kartıyla anında satın alma) | Sitenin birincil CTA'sı demo talebi/iletişim; ödeme entegrasyonu ayrı, daha sonraki bir faz |
|
||||||
|
| Uygulama indirme/dağıtım sayfası (public download) | Uygulama henüz pilot aşamasında; herkese açık indirme linki vermek erken |
|
||||||
|
| Çoklu dil desteği (İngilizce vb.) | Hedef kitle (Türk avukatlar) tek dilli; MVP'de kapsam dışı |
|
||||||
|
| Blog / SEO içerik merkezi | Değerli olabilir ama MVP'nin kapsamı değil; ayrı bir içerik stratejisi gerektirir |
|
||||||
|
| Müşteri girişi / hesap paneli sitede | Bu, uygulamanın kendisinin işi; site sadece tanıtım katmanı |
|
||||||
|
| KVKK/ISO/SOC2 gibi uyumluluk rozetlerinin sitede öne çıkarılması | Ana uygulama PRD'sinde bu konu henüz netleşmedi ("Açık Sorular" bkz. `docs/prd.md` §7) — henüz doğrulanmamış uyumluluk iddiaları sitede öne sürülmemeli |
|
||||||
|
| Canlı chat / chatbot desteği | MVP'de form yeterli; canlı destek ayrı bir araç/süreç gerektirir |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Kullanıcı Hikayeleri
|
||||||
|
|
||||||
|
**Birincil persona: Solo/küçük büro avukatı (site ziyaretçisi)**
|
||||||
|
|
||||||
|
- Ziyaretçi olarak, siteye geldiğimde AyrisLegal'in ne yaptığını ve benim işime nasıl yaradığını hemen anlamak istiyorum, böylece devam edip etmeyeceğime hızlıca karar verebileyim.
|
||||||
|
- Ziyaretçi olarak, ürünün somut özelliklerini (dosya analizi, AI ile tartışma, emsal karar arama) görmek istiyorum, böylece günlük işime gerçekten katkı sağlayıp sağlamayacağını değerlendirebileyim.
|
||||||
|
- Ziyaretçi olarak, demo talep etmek veya soru sormak için kolay bulunabilir, az alanlı bir form doldurmak istiyorum, böylece iletişime geçmek zahmetli olmasın.
|
||||||
|
- Ziyaretçi olarak (güven), ürünü kimin geliştirdiğini ve verilerimin nasıl ele alındığını görmek istiyorum, böylece hassas müvekkil verisiyle çalışan bir araca güvenip güvenemeyeceğimi değerlendirebileyim.
|
||||||
|
- Ziyaretçi olarak, mobilde de siteyi düzgün görüntüleyebilmek istiyorum, böylece telefonumdan da göz atabileyim.
|
||||||
|
|
||||||
|
**İkincil persona: İş sahibi (Mustafa, site yöneticisi)**
|
||||||
|
|
||||||
|
- İş sahibi olarak, forma gelen talepleri (e-posta bildirimi veya basit bir liste ile) görebilmek istiyorum, böylece pilot adaylarla hızlıca iletişime geçebileyim.
|
||||||
|
- İş sahibi olarak, sitenin metin/görsellerini kod bilmeden kolayca güncelleyebilmek istiyorum (ya da en azından basit bir düzenleme süreciyle), böylece mesajlaşmayı pilot geri bildirimine göre hızlıca iterasyonlayabileyim.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Gereksinimler
|
||||||
|
|
||||||
|
### Must-Have (P0)
|
||||||
|
|
||||||
|
**P0-1: Ana Sayfa (Hero + Değer Önerisi)**
|
||||||
|
- Net bir başlık ve alt başlıkla ürünün ne olduğu ve kime hitap ettiği anlatılır ("pasif arama aracı değil, aktif düşünce ortağı" mesajı öne çıkar)
|
||||||
|
- Kabul kriterleri:
|
||||||
|
- [ ] Sayfa açıldığında scroll yapmadan (above the fold) başlık, alt başlık ve birincil CTA butonu görünür
|
||||||
|
- [ ] CTA butonu ("Demo Talep Et" / "İletişime Geç") iletişim formuna yönlendirir/scroll eder
|
||||||
|
|
||||||
|
**P0-2: Özellikler Bölümü**
|
||||||
|
- Ana uygulamanın P0 özellikleri (dosya yükleme + AI analiz, çok turlu sohbet/tartışma, emsal karar arama) somut, anlaşılır dille anlatılır
|
||||||
|
- Kabul kriterleri:
|
||||||
|
- [ ] Her özellik için kısa başlık + açıklama + görsel/ikon
|
||||||
|
- [ ] "Yakında" (dilekçe üretimi, UYAP entegrasyonu gibi Faz 2/3/6 özellikleri) ayrıca ve net şekilde işaretlenir — henüz var olmayan bir şey "mevcut" gibi sunulmaz
|
||||||
|
|
||||||
|
**P0-3: Demo Talebi / İletişim Formu**
|
||||||
|
- Ziyaretçi ad, e-posta, (opsiyonel) baro sicil no ve kısa mesaj alanlarıyla form doldurur
|
||||||
|
- Kabul kriterleri:
|
||||||
|
- [ ] Form gönderimi başarılı olduğunda ziyaretçiye net bir onay mesajı gösterilir
|
||||||
|
- [ ] Gönderilen talep, iş sahibinin görebileceği bir yere ulaşır (e-posta bildirimi ve/veya basit bir veritabanı kaydı)
|
||||||
|
- [ ] Zorunlu alan doldurulmazsa anlaşılır bir hata mesajı gösterilir
|
||||||
|
- [ ] Form spam/bot doldurmalarına karşı temel bir korumaya sahiptir (örn. honeypot alanı veya rate limiting)
|
||||||
|
|
||||||
|
**P0-4: Marka Kimliği Uygulaması**
|
||||||
|
- Site, AyrisLegal logosu (`public/branding/logo-mark.png` kaynaklı) ve lacivert/teal renk paletiyle tutarlı görünür
|
||||||
|
- Kabul kriterleri:
|
||||||
|
- [ ] Logo header'da ve favicon'da kullanılır
|
||||||
|
- [ ] Renk paleti (lacivert arka plan/teal vurgu) ana uygulamanın markasıyla tutarlıdır
|
||||||
|
- [ ] Archivo font ailesi (veya markaya uygun bir alternatif) kullanılır
|
||||||
|
|
||||||
|
**P0-5: Mobil Uyumluluk**
|
||||||
|
- Site mobil tarayıcılarda düzgün görüntülenir ve kullanılabilir
|
||||||
|
- Kabul kriterleri:
|
||||||
|
- [ ] 375px genişliğinden itibaren layout bozulmaz, yatay scroll oluşmaz
|
||||||
|
- [ ] Form mobilde doldurulabilir ve gönderilebilir
|
||||||
|
|
||||||
|
**P0-6: Temel Sayfa Yapısı**
|
||||||
|
- Ana Sayfa, Özellikler, İletişim/Demo (form) — en az bu üç bölüm tek sayfa (single-page) veya ayrı route olarak mevcut
|
||||||
|
- Kabul kriterleri:
|
||||||
|
- [ ] Navigasyon (header menü veya scroll-link) ile bölümler arası geçiş yapılabilir
|
||||||
|
- [ ] Sayfa 3 saniyenin altında yüklenir (temel performans beklentisi)
|
||||||
|
|
||||||
|
### Nice-to-Have (P1)
|
||||||
|
|
||||||
|
- Hakkımızda/Ekip bölümü (kurucu hikayesi, güven inşası için)
|
||||||
|
- Basit SSS (Sık Sorulan Sorular) bölümü — özellikle veri güvenliği/gizlilik soruları
|
||||||
|
- Fiyatlandırma bölümü (sabit 40.000 TL/yıl fiyat noktasını netleştirmeden önce "Fiyat için iletişime geçin" şeklinde belirsiz bırakılabilir)
|
||||||
|
- Basit analitik entegrasyonu (kaç ziyaretçi, hangi sayfadan form doldurdu gibi temel ölçüm)
|
||||||
|
- Ekran görüntüsü/kısa demo videosu galerisi
|
||||||
|
|
||||||
|
### Future Considerations (P2)
|
||||||
|
|
||||||
|
- Blog / SEO içerik stratejisi
|
||||||
|
- Çoklu dil desteği
|
||||||
|
- Self-serve deneme/kayıt akışı (ana uygulamanın lisans/ödeme altyapısı olgunlaştığında)
|
||||||
|
- Müşteri referansları / vaka analizleri (yeterli pilot kullanıcı biriktikten sonra)
|
||||||
|
- Canlı destek/chatbot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Başarı Metrikleri
|
||||||
|
|
||||||
|
**Leading (hızlı değişen göstergeler):**
|
||||||
|
- Form doldurma oranı (ziyaretçi → demo talebi): hedef ilk ayda ölçülebilir bir taban çizgisi oluşturmak (belirli bir yüzde hedefi için trafik verisi birikmesi bekleniyor)
|
||||||
|
- Sayfa yüklenme süresi: hedef <3 saniye
|
||||||
|
- Mobil ziyaretçi oranı ve mobil form tamamlama oranı
|
||||||
|
|
||||||
|
**Lagging (zamanla gelişen göstergeler):**
|
||||||
|
- Form üzerinden gelen taleplerden gerçek pilot kullanıcıya dönüşüm oranı
|
||||||
|
- Ana uygulama PRD'sindeki "ilk 2 ayda 1.000 ödeyen kullanıcı" hedefine sitenin katkısı (form → pilot → ödeyen kullanıcı hunisi)
|
||||||
|
|
||||||
|
**Ölçüm yöntemi:** MVP aşamasında resmi analitik altyapısı yok (P1'de temel analitik eklenmedikçe); form gönderimleri ve basit ziyaretçi sayısı takip edilecek.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Açık Sorular
|
||||||
|
|
||||||
|
- **[Ürün]** Demo talebi formu hangi bilgileri toplamalı — sadece ad/e-posta yeterli mi, yoksa büro büyüklüğü/ihtiyaç türü gibi niteleyici sorular eklensin mi (satış önceliklendirmesi için faydalı olabilir ama form uzarsa tamamlanma oranı düşebilir)?
|
||||||
|
- **[Mühendislik]** Form gönderimleri nereye düşecek — basit bir e-posta servisi (örn. Resend/SendGrid) mi, yoksa `laawos-backend`'e yeni bir endpoint mi eklenecek (mevcut Supabase altyapısına bir `leads`/`demo_requests` tablosu eklemek mantıklı olabilir)?
|
||||||
|
- **[Ürün]** Fiyatlandırma sitede hiç gösterilsin mi, yoksa "Fiyat için iletişime geçin" şeklinde mi bırakılsın? Ana PRD'de 40.000 TL/yıl fiyat noktası henüz "satış denemeleriyle doğrulanacak" aşamada.
|
||||||
|
- **[Tasarım]** Site, ana uygulamanın kendi `page.tsx`'indeki inline-style tabanlı tasarım sistemini mi (aynı `DS` token yaklaşımı) paylaşacak, yoksa bağımsız bir stack (örn. Next.js + Tailwind, ayrı bir repo) mi kullanılacak?
|
||||||
|
- **[Hukuki]** Sitede "yakında" olarak işaretlenecek özellikler (dilekçe üretimi, UYAP entegrasyonu) ile ilgili herhangi bir zaman taahhüdü verilmeli mi, yoksa tarihsiz mi bırakılsın?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Teknik Notlar
|
||||||
|
|
||||||
|
- **Stack önerisi:** Next.js (statik export veya basit hosting), ana uygulamayla aynı marka varlıklarını (`public/branding/logo-mark.png`, `logo-branding.png`) paylaşabilir. Bağımsız bir repo olarak kurulması (ana Electron uygulamasından ayrı deploy edilebilmesi için) önerilir.
|
||||||
|
- **Deploy:** `coolify-deploy` skill'iyle uyumlu olacak şekilde, Gitea reposundan Coolify'a otomatik build — ana backend ile aynı altyapı deseni.
|
||||||
|
- **Form backend'i:** Basit bir POST endpoint (yeni veya `laawos-backend`'e eklenen bir route) veya üçüncü taraf form servisi (Formspree, Resend) — Açık Sorular §7'de karar bekliyor.
|
||||||
|
- **SEO temelleri:** Meta title/description, Open Graph görselleri (logo/marka görseliyle), temel semantik HTML — P0 kapsamında minimum, P2'de genişletilebilir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Zaman Çizelgesi
|
||||||
|
|
||||||
|
| Faz | Süre | Bu PRD'nin Kapsamı |
|
||||||
|
|---|---|---|
|
||||||
|
| Faz 1 — Tanıtım Sitesi MVP (bu doküman) | Birkaç gün | P0-1 → P0-6 |
|
||||||
|
| Faz 2 — Güven/İçerik Katmanı | Sonrası | P1 maddeleri (SSS, Hakkımızda, fiyatlandırma netleşmesi) |
|
||||||
|
| Faz 3 — Büyüme/SEO | Ayrı PRD gerekir | P2 maddeleri (blog, self-serve, çoklu dil) |
|
||||||
|
|
||||||
|
**Bağımsızlık notu:** Bu site, ana AyrisLegal uygulamasının geliştirme sürecinden bağımsız olarak paralel yürütülebilir; tek bağımlılığı marka varlıklarının (logo, renk paleti) hazır olmasıdır — ki zaten mevcut.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {getRequestConfig} from 'next-intl/server';
|
||||||
|
import {routing} from './routing';
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({requestLocale}) => {
|
||||||
|
let locale = await requestLocale;
|
||||||
|
|
||||||
|
if (!locale || !routing.locales.includes(locale as any)) {
|
||||||
|
locale = routing.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../messages/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {defineRouting} from 'next-intl/routing';
|
||||||
|
import {createNavigation} from 'next-intl/navigation';
|
||||||
|
|
||||||
|
export const routing = defineRouting({
|
||||||
|
locales: ['en', 'tr', 'de'],
|
||||||
|
defaultLocale: 'tr'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {Link, redirect, usePathname, useRouter, getPathname} =
|
||||||
|
createNavigation(routing);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import NextAuth from "next-auth"
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
|
|
||||||
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||||
|
providers: [
|
||||||
|
CredentialsProvider({
|
||||||
|
name: "Credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" }
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
// Boilerplate mock logic
|
||||||
|
// TODO: In production, lookup user in Prisma and verify password using bcrypt
|
||||||
|
// const user = await db.user.findUnique({ where: { email: credentials.email } })
|
||||||
|
|
||||||
|
if (credentials?.email === "admin@ayris.tech" && credentials?.password === "admin") {
|
||||||
|
return {
|
||||||
|
id: "1",
|
||||||
|
name: "Admin User",
|
||||||
|
email: "admin@ayris.tech",
|
||||||
|
role: "ADMIN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
if (user) {
|
||||||
|
token.role = (user as any).role
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
if (session.user && token.role) {
|
||||||
|
(session.user as any).role = token.role
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/login'
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { v2 as cloudinary } from 'cloudinary'
|
||||||
|
|
||||||
|
cloudinary.config({
|
||||||
|
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
|
||||||
|
api_key: process.env.CLOUDINARY_API_KEY!,
|
||||||
|
api_secret: process.env.CLOUDINARY_API_SECRET!,
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function uploadImage(file: string, folder: string) {
|
||||||
|
const result = await cloudinary.uploader.upload(file, {
|
||||||
|
folder, transformation: [{ quality: 'auto', fetch_format: 'auto' }],
|
||||||
|
})
|
||||||
|
return { url: result.secure_url, publicId: result.public_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteImage(publicId: string) {
|
||||||
|
await cloudinary.uploader.destroy(publicId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { cloudinary }
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prisma: PrismaClient | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = globalForPrisma.prisma ?? new PrismaClient()
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"about": "About Us",
|
||||||
|
"contact": "Contact"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"title": "Welcome",
|
||||||
|
"cta": "Learn More"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"rights": "All rights reserved."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Ana Sayfa",
|
||||||
|
"features": "Özellikler",
|
||||||
|
"why": "Neden AyrisLegal?",
|
||||||
|
"contact": "İletişim",
|
||||||
|
"login": "Giriş Yap"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"badge": "Avukatlar için yapay zeka destekli hukuki asistan",
|
||||||
|
"title": "Pasif araştırma değil, aktif düşünce ortağı",
|
||||||
|
"subtitle": "AyrisLegal, dosyalarınızı analiz eder, hukuki sorularınızı yanıtlar ve emsal kararları saniyeler içinde bulur.",
|
||||||
|
"cta": "Demo Talep Et",
|
||||||
|
"secondary_cta": "Özellikleri Keşfet"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"rights": "Tüm hakları saklıdır.",
|
||||||
|
"created_by": "Created by ayris.tech"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { NextConfig } from 'next'
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin'
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{ protocol: 'https', hostname: 'res.cloudinary.com' },
|
||||||
|
{ protocol: 'https', hostname: 'images.unsplash.com' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withNextIntl(nextConfig)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "ayrislegal",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "AyrisLegal - Hukuki Yönetim Platformu",
|
||||||
|
"author": "ayris.tech",
|
||||||
|
"homepage": "https://ayris.tech",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.5.0",
|
||||||
|
"@prisma/client": "^7.8.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"cloudinary": "^2.10.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"developer-icons": "^7.0.1",
|
||||||
|
"framer-motion": "^12.40.0",
|
||||||
|
"lucide-react": "^1.18.0",
|
||||||
|
"next": "16.2.9",
|
||||||
|
"next-auth": "^5.0.0-beta.31",
|
||||||
|
"next-intl": "^4.13.0",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"shadcn": "^4.11.0",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tw-animate-css": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.2.9",
|
||||||
|
"prisma": "^7.8.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String?
|
||||||
|
email String @unique
|
||||||
|
password String?
|
||||||
|
role Role @default(USER)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
providerAccountId String
|
||||||
|
refresh_token String? @db.Text
|
||||||
|
access_token String? @db.Text
|
||||||
|
expires_at Int?
|
||||||
|
token_type String?
|
||||||
|
scope String?
|
||||||
|
id_token String? @db.Text
|
||||||
|
session_state String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([provider, providerAccountId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionToken String @unique
|
||||||
|
userId String
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String @unique
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@unique([identifier, token])
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import createMiddleware from 'next-intl/middleware'
|
||||||
|
import { auth } from '@/lib/auth'
|
||||||
|
import { routing } from '@/i18n/routing'
|
||||||
|
|
||||||
|
const intlMiddleware = createMiddleware(routing)
|
||||||
|
|
||||||
|
export async function proxy(request: NextRequest) {
|
||||||
|
if (request.nextUrl.pathname.includes('/admin')) {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session || (session.user as any)?.role !== 'ADMIN') {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intlMiddleware(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 600 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||