first commit
This commit is contained in:
@@ -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 }
|
||||
Reference in New Issue
Block a user