Connect Meshkee backend, blog and shared account session
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
import {useEffect,useState,useRef} from 'react';
|
||||
import {UserRound,ChevronDown} from 'lucide-react';
|
||||
import {CUSTOMER_URL,CUSTOMER_PROFILE_URL,WEBSITE_MANAGEMENT_URL} from '../../lib/meshkee/api';
|
||||
export default function AccountMenu(){
|
||||
const [user,setUser]=useState<{name:string;isAdmin:boolean}|null>(null);const [open,setOpen]=useState(false);const [busy,setBusy]=useState(false);const [error,setError]=useState('');const root=useRef<HTMLDivElement>(null);const button=useRef<HTMLButtonElement>(null);
|
||||
useEffect(()=>{const controller=new AbortController();fetch('/api/account',{cache:'no-store',signal:controller.signal}).then(r=>r.json()).then(data=>setUser(data.user||null)).catch(()=>{});return()=>controller.abort()},[]);
|
||||
useEffect(()=>{if(!open)return;const outside=(event:PointerEvent)=>{if(!root.current?.contains(event.target as Node))setOpen(false)};const key=(event:KeyboardEvent)=>{if(event.key==='Escape'){setOpen(false);button.current?.focus()}};document.addEventListener('pointerdown',outside);document.addEventListener('keydown',key);return()=>{document.removeEventListener('pointerdown',outside);document.removeEventListener('keydown',key)}},[open]);
|
||||
async function logout(){setBusy(true);setError('');try{const response=await fetch('/api/logout',{method:'POST'});if(!response.ok)throw new Error();setUser(null);setOpen(false)}catch{setError('خروج انجام نشد؛ دوباره تلاش کنید.')}finally{setBusy(false)}}
|
||||
if(!user)return <a className="account-login" href={CUSTOMER_URL} aria-label="ورود به حساب کاربری"><UserRound size={20}/><span>ورود</span></a>;
|
||||
return <div className="account-menu" ref={root} onBlur={e=>{if(!e.currentTarget.contains(e.relatedTarget as Node))setOpen(false)}}><button ref={button} type="button" className="account-login" aria-expanded={open} aria-controls="account-dropdown" onClick={()=>setOpen(!open)}><UserRound size={19}/>{user.name} عزیز<ChevronDown size={14}/></button>{open&&<div id="account-dropdown" className="account-dropdown"><a href={CUSTOMER_PROFILE_URL}>پروفایل من</a>{user.isAdmin&&<a href={WEBSITE_MANAGEMENT_URL}>مدیریت وبسایت</a>}<button disabled={busy} onClick={logout}>{busy?'در حال خروج…':'خروج'}</button>{error&&<p role="alert">{error}</p>}</div>}</div>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { Send, CheckCircle2, X } from 'lucide-react';
|
||||
import { validateContact } from '../../lib/meshkee/contact';
|
||||
export default function ContactForm(){
|
||||
const [busy,setBusy]=useState(false);const [errors,setErrors]=useState<Record<string,string>>({});const [message,setMessage]=useState('');const [success,setSuccess]=useState(false);const [toast,setToast]=useState(false);const locked=useRef(false);
|
||||
useEffect(()=>{if(!toast)return;const timer=setTimeout(()=>setToast(false),6500);return()=>clearTimeout(timer)},[toast]);
|
||||
async function submit(event:React.FormEvent<HTMLFormElement>){
|
||||
event.preventDefault();if(locked.current)return;
|
||||
const form=event.currentTarget;const {data,errors:validation}=validateContact(Object.fromEntries(new FormData(form)));
|
||||
setErrors(validation);setMessage('');setSuccess(false);setToast(false);
|
||||
if(Object.keys(validation).length){(form.elements.namedItem(Object.keys(validation)[0]) as HTMLElement)?.focus();return}
|
||||
locked.current=true;setBusy(true);
|
||||
try{const response=await fetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data),signal:AbortSignal.timeout(30000)});const result=await response.json();if(!response.ok){setErrors(result.errors||{});throw new Error(result.message||'ارسال پیام ممکن نشد.')};setMessage(result.message);setSuccess(true);setToast(true);form.reset();}
|
||||
catch(error){setMessage(error instanceof Error&&error.name!=='TimeoutError'?error.message:'ارتباط برقرار نشد. لطفاً دوباره تلاش کنید.');}
|
||||
finally{locked.current=false;setBusy(false)}
|
||||
}
|
||||
return <section className="contact-form-section"><div><span className="eyebrow">SEND A MESSAGE</span><h2>برای ما بنویسید</h2><p>پرسش یا درخواست خود را با کارشناسان ما در میان بگذارید.</p></div><form onSubmit={submit} noValidate aria-busy={busy}><fieldset disabled={busy}><div className="form-grid">{[{name:'name',label:'نام و نام خانوادگی',required:true,max:120},{name:'title',label:'موضوع پیام',required:true,max:200},{name:'email',label:'پست الکترونیکی',type:'email',max:254},{name:'cellNumber',label:'شماره تماس',type:'tel',max:25}].map(field=><label key={field.name} htmlFor={'contact-'+field.name}>{field.label}{field.required&&' *'}<input id={'contact-'+field.name} name={field.name} type={field.type||'text'} required={field.required} maxLength={field.max} autoComplete={field.name==='name'?'name':field.type==='email'?'email':field.type==='tel'?'tel':'off'} dir={field.type?'ltr':undefined} aria-invalid={Boolean(errors[field.name])} aria-describedby={errors[field.name]?'error-'+field.name:undefined}/>{errors[field.name]&&<small id={'error-'+field.name} className="field-error">{errors[field.name]}</small>}</label>)}</div><label htmlFor="contact-text">متن پیام *<textarea id="contact-text" name="text" rows={5} required maxLength={5000} aria-invalid={Boolean(errors.text)} aria-describedby={errors.text?'error-text':undefined}/>{errors.text&&<small id="error-text" className="field-error">{errors.text}</small>}</label><button className="button" type="submit">{busy?'در حال ارسال…':'ارسال پیام'}<Send size={18}/></button></fieldset>{message&&<p className={success?'form-success':'field-error'} role={success?'status':'alert'}>{message}</p>}</form>{toast&&<div className="success-toast"><CheckCircle2 size={22}/><span>پیام شما با موفقیت ثبت شد.</span><button type="button" onClick={()=>setToast(false)} aria-label="بستن اعلان"><X size={18}/></button></div>}</section>
|
||||
}
|
||||
Reference in New Issue
Block a user