پروژه کامل: فرانت متصل به API + داکرایز
- اتصال کامل فرانت به API: همه محتوای سایت (پروژهها، مقالات، خدمات، برند و...) از بکاند خوانده میشود (SiteProvider + useSite) - فرم تماس و خبرنامه متصل به بکاند و تستشده در مرورگر - ساختار مونوریپو: frontend/ + backend/ - Dockerfile برای هر دو بخش + docker-compose (PostgreSQL + FastAPI + nginx) - nginx با پروکسی /api — بدون نیاز به CORS در حالت داکر - README کامل فارسی
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { MoveHorizontalIcon } from 'lucide-react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { SectionHeading } from '../SectionHeading'
|
||||
import { useSite } from '../../api'
|
||||
|
||||
export function BeforeAfter() {
|
||||
const { before_after } = useSite()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [position, setPosition] = useState(52)
|
||||
const dragging = useRef(false)
|
||||
|
||||
const setFromClientX = useCallback((clientX: number) => {
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const ratio = ((clientX - rect.left) / rect.width) * 100
|
||||
setPosition(Math.min(98, Math.max(2, ratio)))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const move = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dragging.current) return
|
||||
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
|
||||
setFromClientX(clientX)
|
||||
}
|
||||
const stop = () => {
|
||||
dragging.current = false
|
||||
}
|
||||
window.addEventListener('mousemove', move)
|
||||
window.addEventListener('touchmove', move)
|
||||
window.addEventListener('mouseup', stop)
|
||||
window.addEventListener('touchend', stop)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', move)
|
||||
window.removeEventListener('touchmove', move)
|
||||
window.removeEventListener('mouseup', stop)
|
||||
window.removeEventListener('touchend', stop)
|
||||
}
|
||||
}, [setFromClientX])
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'ArrowLeft') setPosition((p) => Math.max(2, p - 4))
|
||||
if (e.key === 'ArrowRight') setPosition((p) => Math.min(98, p + 4))
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-char py-24 lg:py-32" aria-labelledby="ba-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
|
||||
<SectionHeading
|
||||
title={before_after.title}
|
||||
lead={before_after.lead}
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<Reveal delay={0.1}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative mx-auto mt-14 aspect-[4/3] w-full max-w-4xl select-none overflow-hidden rounded-xl border border-ivory/10 shadow-2xl md:aspect-[16/10]"
|
||||
onMouseDown={(e) => {
|
||||
dragging.current = true
|
||||
setFromClientX(e.clientX)
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
dragging.current = true
|
||||
setFromClientX(e.touches[0].clientX)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={before_after.before}
|
||||
alt={before_after.before_label}
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 overflow-hidden" style={{ clipPath: `inset(0 0 0 ${position}%)` }} aria-hidden="true">
|
||||
<img
|
||||
src={before_after.after}
|
||||
alt=""
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="pointer-events-none absolute left-6 top-6 z-30 rounded-md bg-ink/85 px-4 py-2 text-xs font-light text-ivory backdrop-blur-md border border-ivory/10 shadow-lg">
|
||||
{before_after.before_label}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute right-6 top-6 z-30 rounded-md bg-ink/85 px-4 py-2 text-xs font-light text-copper backdrop-blur-md border border-copper/30 shadow-lg">
|
||||
{before_after.after_label}
|
||||
</span>
|
||||
|
||||
<div className="pointer-events-none absolute inset-y-0 z-30" style={{ left: `${position}%` }}>
|
||||
<div className="h-full w-[2px] bg-copper shadow-[0_0_12px_rgba(213,135,76,0.9)]" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
role="slider"
|
||||
aria-label="مقایسه کانسپت و پروژه ساختهشده"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={Math.round(position)}
|
||||
onKeyDown={onKeyDown}
|
||||
onMouseDown={() => {
|
||||
dragging.current = true
|
||||
}}
|
||||
onTouchStart={() => {
|
||||
dragging.current = true
|
||||
}}
|
||||
className="absolute top-1/2 z-40 flex h-14 w-14 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize items-center justify-center rounded-full bg-copper text-char shadow-2xl transition-transform duration-200 ease-lux hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-ivory"
|
||||
style={{ left: `${position}%` }}
|
||||
>
|
||||
<MoveHorizontalIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user