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>
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowDownLeft, MoveLeft, ShieldCheck, Pause, Play, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
const slides = [
|
||||
{image:'laboratory.jpg',alt:'محیط آزمایشگاهی و تجهیزات تخصصی',title:'اعتماد، نخستین گام',line:'در مسیر',accent:'سلامت.',description:'راهکارهای تخصصی استریلیزاسیون و کنترل عفونت؛',detail:'همراه مراکز درمانی، از انتخاب تا استفاده.',caption:'دانش، دقت، اعتماد',en:'SCIENCE IN SERVICE OF HEALTH',cta:'محصولات تخصصی ما',href:'#products'},
|
||||
{image:'care.jpg',alt:'متخصصان در محیط پزشکی',title:'همراه شما، در مسیر',line:'مراقبت',accent:'آگاهانه.',description:'محصولات تخصصی بهداشت و کنترل عفونت؛',detail:'برای بیمارستانها، آزمایشگاهها و مراکز درمانی.',caption:'همراه مراکز درمانی',en:'INFECTION CONTROL',cta:'مشاوره و انتخاب محصول',href:'/tmas-bama'},
|
||||
{image:'research.jpg',alt:'پژوهش و فعالیت در آزمایشگاه',title:'دانش، پایهٔ پیشرفت',line:'و انتخاب',accent:'مطمئن.',description:'آموزش، کلید توسعه و تخصص ما است؛',detail:'هر روز، بهتر از دیروز.',caption:'بهبود مستمر، با دانش و تجربه',en:'KNOWLEDGE AND EXPERIENCE',cta:'آشنایی با سلامت گستر',href:'/about-us'},
|
||||
];
|
||||
export default function HeroSlider(){
|
||||
const [index,setIndex]=useState(0);
|
||||
const [paused,setPaused]=useState(false);
|
||||
const [hovered,setHovered]=useState(false);
|
||||
const [reduced,setReduced]=useState(true);
|
||||
const [hidden,setHidden]=useState(false);
|
||||
useEffect(()=>{const query=window.matchMedia('(prefers-reduced-motion: reduce)');const sync=()=>setReduced(query.matches);const visibility=()=>setHidden(document.hidden);sync();visibility();query.addEventListener('change',sync);document.addEventListener('visibilitychange',visibility);return()=>{query.removeEventListener('change',sync);document.removeEventListener('visibilitychange',visibility)}},[]);
|
||||
const running=!paused&&!hovered&&!reduced&&!hidden;
|
||||
useEffect(()=>{if(!running)return;const timer=setTimeout(()=>setIndex(i=>(i+1)%slides.length),6500);return()=>clearTimeout(timer)},[index,running]);
|
||||
const choose=(next:number)=>{setPaused(true);setIndex((next+slides.length)%slides.length)};
|
||||
const slide=slides[index];
|
||||
return <section className="hero hero-slider" aria-label="معرفی سلامت گستر اعتماد" aria-roledescription="اسلایدر" onMouseEnter={()=>setHovered(true)} onMouseLeave={()=>setHovered(false)} onFocusCapture={()=>setPaused(true)}>
|
||||
<div className="hero-photo"><div className="hero-images">{slides.map((s,i)=><img key={s.image} src={'/images/'+s.image} alt={i===index?s.alt:''} aria-hidden={i!==index} className={i===index?'active':''} loading="eager"/>)}</div><div key={index} className="photo-label"><span className="cross">+</span><div>{slide.caption}<small>{slide.en}</small></div></div></div>
|
||||
<div className="hero-copy"><div key={index} className="hero-sequence" aria-live={paused?'polite':'off'} aria-atomic="true"><span className="eyebrow">SGE MEDICAL GROUP <i/></span><h1><span>{slide.title}</span><span>{slide.line} <em>{slide.accent}</em></span></h1><p>{slide.description}<br/>{slide.detail}</p><div className="actions"><Link className="button" href={slide.href}>{slide.cta}<MoveLeft size={19}/></Link><Link className="textlink" href="/about-us">درباره ما <ArrowDownLeft size={19}/></Link></div></div>
|
||||
<div className="hero-bottom"><ShieldCheck size={25}/><span>شرکت مهندسی سلامت گستر اعتماد<small>مشاور و مجری استریل و کنترل عفونت</small></span></div>
|
||||
<div className="slider-controls"><div className="slider-dots">{slides.map((s,i)=><button key={s.image} aria-label={'اسلاید '+(i+1)+': '+s.caption} aria-current={index===i?'true':undefined} onClick={()=>choose(i)}><span className={index===i?'selected':''}/></button>)}</div><span className="slide-count" dir="ltr">0{index+1} / 03</span><div className="slider-arrows"><button aria-label="اسلاید قبلی" onClick={()=>choose(index-1)}><ChevronRight size={18}/></button><button aria-label={paused?'پخش خودکار':'توقف پخش خودکار'} aria-pressed={paused} disabled={reduced} onClick={()=>setPaused(p=>!p)}>{paused||reduced?<Play size={15}/>:<Pause size={15}/>}</button><button aria-label="اسلاید بعدی" onClick={()=>choose(index+1)}><ChevronLeft size={18}/></button></div></div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronDown, ArrowUpLeft, FlaskConical, Microscope, Package, ShieldCheck } from 'lucide-react';
|
||||
import catalog from '../app/catalog.json';
|
||||
|
||||
const groups = [
|
||||
{ title: 'اندیکاتورهای شیمیایی', icon: FlaskConical, start: 2, end: 15 },
|
||||
{ title: 'اندیکاتورهای بیولوژیکال', icon: Microscope, start: 0, end: 2 },
|
||||
{ title: 'بستهبندی استریل', icon: Package, start: 15, end: 26 },
|
||||
{ title: 'بهداشت و کنترل عفونت', icon: ShieldCheck, start: 26, end: 30 },
|
||||
];
|
||||
|
||||
export default function ProductMenu() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const root = useRef<HTMLDivElement>(null);
|
||||
const trigger = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function outside(event: PointerEvent) {
|
||||
if (!root.current?.contains(event.target as Node)) setOpen(false);
|
||||
}
|
||||
function escape(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false);
|
||||
trigger.current?.focus();
|
||||
}
|
||||
}
|
||||
document.addEventListener('pointerdown', outside);
|
||||
document.addEventListener('keydown', escape);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', outside);
|
||||
document.removeEventListener('keydown', escape);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return <div className="product-menu" ref={root} onBlur={event => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node)) setOpen(false);
|
||||
}}>
|
||||
<button ref={trigger} className="product-menu-trigger" aria-expanded={open} aria-controls="product-mega-menu" onClick={() => setOpen(!open)}>
|
||||
محصولات <ChevronDown size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<div id="product-mega-menu" className="mega-menu" hidden={!open}>
|
||||
<div className="mega-heading"><div><strong>محصولات تخصصی ما</strong><span>استریلیزاسیون و کنترل عفونت</span></div><Link href="/#products" onClick={() => setOpen(false)}>همه محصولات <ArrowUpLeft size={17} /></Link></div>
|
||||
<div className="mega-columns">{groups.map(group => <section key={group.title}>
|
||||
<h2><group.icon size={21} aria-hidden="true" />{group.title}</h2>
|
||||
<ul>{catalog.slice(group.start, group.end).map(product => <li key={product.source}>
|
||||
<a href={product.downloaded ? product.url : '/tmas-bama'} target={product.downloaded ? '_blank' : undefined} rel={product.downloaded ? 'noreferrer' : undefined} onClick={() => setOpen(false)}>
|
||||
<span>{product.title}</span><small>{product.downloaded ? 'PDF' : 'درخواست'}</small>
|
||||
</a>
|
||||
</li>)}</ul>
|
||||
</section>)}</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user