b
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
# 1. Base image
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# 2. Dependencies
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
|
||||
# 3. Builder
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Environment variables must be present at build time for Next.js
|
||||
# Coolify will provide these, but we can set defaults
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# 4. Runner
|
||||
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
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
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
|
||||
# set hostname to localhost
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
287
app/about/page.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'Mimari Hizmetler',
|
||||
description: 'A.N.Tarchitecture, proaktif entegre tasarım yaklaşımıyla kapsamlı bir profesyonel mimari hizmet yelpazesi sunar:',
|
||||
list: [
|
||||
'Proje Geliştirme: Saha analizi, potansiyel çalışma, sosyal kabul edilebilirlik stratejileri ve planlama',
|
||||
'Plan ve Şartnamelerin Tasarımı ve Üretimi.',
|
||||
'Proje Modelleme Yönetimi: Entegre BIM (Yapı Bilgi Modellemesi) tasarımı',
|
||||
'Düzenleyici Stratejiler: Yönetmelikler, saha çalışması, enerji ve güneş ışığı analizi',
|
||||
'Kentsel Tasarım: Master planlama, peyzaj tasarımı ve inşaat mühendisliği işleri',
|
||||
'3D Görselleştirme: 3D perspektifler, modelleme ve animasyonlar',
|
||||
'İnşaat Hizmeti ve Yönetimi: İnşaat yönetimi ve saha denetimi'
|
||||
],
|
||||
button: 'PROJELERİ GÖRÜNTÜLE',
|
||||
image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070&auto=format&fit=crop'
|
||||
},
|
||||
{
|
||||
title: 'İç Mimari Hizmetleri',
|
||||
description: 'A.N.Tarchitecture, sürdürülebilirlik, zarafet ve işlevselliği şirketinizin değerleriyle uyumlu akıllı düzenlerle birleştirerek, imajını ve işe alım kapasitesini artıran iç mimari konusunda derin bir uzmanlık sunar:',
|
||||
list: [
|
||||
'Aktif Müşteri Dinleme: Alan programlama ve planlama, bütçe analizi',
|
||||
'Konsept Geliştirme: Plan ve şartnamelerin üretimi, malzeme seçimi, entegre mobilya ve özel objelerin kavramsallaştırılması',
|
||||
'Entegre Tabela: Plan ve şartnamelerin modellenmesi ve üretimi',
|
||||
'3D Görselleştirme: 3D perspektifler, modelleme ve animasyonlar'
|
||||
],
|
||||
button: 'TÜM PROJELER',
|
||||
image: 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?q=80&w=2070&auto=format&fit=crop'
|
||||
},
|
||||
{
|
||||
title: 'Planlama ve Uzmanlık Hizmetleri',
|
||||
description: 'A.N.Tarchitecture, bina planlama, programlama ve denetleme konularında, müşteri ihtiyaçlarını ve gereksinimlerini net bir şekilde tanımlamak için titiz ve sistematik yöntemler kullanarak çeşitli uzmanlıklar sunar.',
|
||||
list: [
|
||||
'Fonksiyonel ve Teknik Programların (FTP) Geliştirilmesi: Müdahale stratejileri, organizasyonun genel özellikleri, işlevsellik ve alanlar',
|
||||
'Fizibilite Çalışması ve Ön Konseptler: Teknik ve düzenleyici koşulların ve kısıtlamaların analizi',
|
||||
'Teknik Uzmanlık: Su yalıtım sorunlarının ve erken yaşlanmanın tespiti, yaşam döngüsü analizi'
|
||||
],
|
||||
button: null,
|
||||
image: 'https://images.unsplash.com/photo-1518780664697-55e3ad937233?q=80&w=2070&auto=format&fit=crop'
|
||||
}
|
||||
]
|
||||
|
||||
const projectPhases = [
|
||||
{ num: '00', title: 'ÖN KAVRAMSAL ÇALIŞMALAR', desc: 'Bir binanın kapsamını belirlemek için ihtiyaçlarını ve işlevlerini anlamaya ve belgelemeye yardımcı olan hazırlık aşaması.' },
|
||||
{ num: '01', title: 'TASARIM AŞAMASI', desc: 'Bu aşama saha incelemesi, program incelemesi, ilk eskiz tasarımı ve ön bütçe analizini içerir.' },
|
||||
{ num: '02', title: 'TASARIM GELİŞTİRME', desc: 'Eskizler onaylandıktan sonra, gerekli ön planları, kesitleri ve görünüşleri oluşturarak projeyi daha detaylı geliştiriyoruz.' },
|
||||
{ num: '03', title: 'İNŞAAT BELGELERİ', desc: 'Bu inşaat için gerekli olan teknik planların gerçek geliştirilme aşamasıdır.' },
|
||||
{ num: '04', title: 'İHALE', desc: 'Genel yüklenici tekliflerinin analizinde ve inşaat sözleşmesinin imzalanmasında müşteriye yardımcı oluyor ve danışmanlık yapıyoruz.' },
|
||||
{ num: '05', title: 'SAHA DENETİMİ', desc: 'Projenin planlara sadık kalarak yürütülmesini sağlıyoruz. İnşaat sahasındaki öngörülemeyen sorunlara tasarımı uyarlamak için çözümler sunuyoruz.' }
|
||||
]
|
||||
|
||||
export default function AboutPage() {
|
||||
const targetRef = useRef<HTMLDivElement>(null)
|
||||
const { scrollY } = useScroll()
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const windowHeight = window.innerHeight
|
||||
const documentHeight = document.documentElement.scrollHeight
|
||||
const scrollPosition = window.scrollY + windowHeight
|
||||
setIsAtBottom(scrollPosition > documentHeight - 150)
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
const cargoFontSize = useTransform(scrollY, [0, 300], ["10vw", "4vw"])
|
||||
const archFontSize = useTransform(scrollY, [0, 300], ["8vw", "3vw"])
|
||||
const bottomPadding = useTransform(scrollY, [0, 300], ["2.5rem", "1rem"])
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: targetRef,
|
||||
offset: ["start start", "end end"]
|
||||
})
|
||||
|
||||
return (
|
||||
<main className="relative bg-[#0a0a0a] text-white selection:bg-white selection:text-black" style={{ overflowX: 'clip' }}>
|
||||
|
||||
{/* SECTION 1: Intro */}
|
||||
<section className="relative min-h-screen flex items-center justify-center pt-40 pb-32 px-6 md:px-16">
|
||||
<div className="max-w-[1400px] w-full grid grid-cols-1 lg:grid-cols-[1fr_400px] gap-12 lg:gap-24 items-start">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<p className="text-[24px] md:text-[36px] lg:text-[42px] leading-[1.2] font-normal tracking-tight text-white/95">
|
||||
Mimariyi, teknik disiplin ile yaratıcı vizyonun kusursuz dengesi olarak tanımlıyorum. Tasarım sürecimde hem iç hem de dış mekân projelerinde bütüncül bir yaklaşımı benimsiyor; projelerimi sadece birer yapı olarak değil, çevresiyle nefes alan yaşam alanları olarak tasarlıyorum.
|
||||
|
||||
Projelerimin teknik omurgasını AutoCAD üzerinde detaylı plan ve kesit çalışmalarıyla oluştururken, 3D modelleme ve görselleştirme süreçlerinde sunduğum gerçekçi sunumlarla tasarıma derinlik kazandırıyorum. Konsept tasarımından uygulama detaylarına kadar her aşamada, mekânın işlevselliğini estetik bir değerle harmanlamayı amaçlıyorum. </p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.2, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="flex flex-col space-y-12 lg:space-y-16 mt-4"
|
||||
>
|
||||
<p className="text-[13px] md:text-[14px] leading-relaxed text-zinc-400 font-medium">
|
||||
2006 yılında Toronto'da CGBWstudio adıyla Charles-Bernard Gagnon tarafından kurulan ve 2008'de Quebec City'ye taşınan firma, felsefesi ve sürecin her aşamasında yüksek kaliteli profesyonel hizmet sunma taahhüdü sayesinde bölge pazarında öne çıkmıştır. Bu yaklaşım, firmanın yenilikçi projelerden oluşan bir portföy oluşturmasını sağlamıştır. 2013 yılında ekip, daha anlamlı bir isim olan A.N.Tarchitecture adını seçmiştir.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="relative w-full aspect-[3/4] rounded-[1px] overflow-hidden grayscale border border-white/10 group max-w-[300px] lg:max-w-none">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1595392756136-ba72c36bcfe0?q=80&w=1974&auto=format&fit=crop"
|
||||
alt="Ayça Nur Turhan"
|
||||
fill
|
||||
sizes="(max-width: 1024px) 300px, 400px"
|
||||
className="object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] md:text-[11px] font-bold uppercase tracking-[0.2em] leading-[1.5] text-white/60">
|
||||
AYÇA NUR TURHAN KURUCU <br />MİMAR, OAQ
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 2: Services – Sequential Scroll Animation */}
|
||||
<section ref={targetRef} className="relative md:h-[180vh] bg-[#0a0a0a]">
|
||||
<div className="hidden md:block sticky top-0 h-screen w-full p-4">
|
||||
<div className="grid grid-cols-3 gap-4 w-full h-full">
|
||||
{services.map((service, index) => (
|
||||
<ServiceCard
|
||||
key={index}
|
||||
index={index}
|
||||
service={service}
|
||||
scrollYProgress={scrollYProgress}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Vertical Stack */}
|
||||
<div className="md:hidden flex flex-col space-y-1 px-4 pb-20">
|
||||
{services.map((service, index) => (
|
||||
<ServiceCardMobile key={index} service={service} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 3: Project Phases */}
|
||||
<section className="relative min-h-screen bg-[#0a0a0a] pt-24 md:pt-40 pb-40 md:pb-60 px-6 md:px-16 border-t border-white/10">
|
||||
<div className="max-w-[1400px] mx-auto grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-12 md:gap-20">
|
||||
<div className="flex flex-col items-start space-y-6 md:space-y-8">
|
||||
<h2 className="text-[36px] md:text-[52px] font-normal tracking-tight">Proje Aşamaları</h2>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="bg-white text-black px-6 py-2.5 text-[10px] font-extrabold tracking-[0.2em] uppercase hover:bg-zinc-200 transition-colors rounded-[1px]"
|
||||
>
|
||||
PROJENİZİ BAŞLATIN
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{projectPhases.map((phase, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="grid grid-cols-1 sm:grid-cols-[60px_1.2fr_2fr] gap-4 md:gap-8 py-8 md:py-10 border-t border-white/20 first:border-t-0 px-4 md:px-6 -mx-4 md:-mx-6 hover:bg-white hover:text-black transition-all duration-300 group cursor-default"
|
||||
>
|
||||
<div className="text-[12px] font-bold text-white/40 group-hover:text-black/40">{phase.num}</div>
|
||||
<div className="text-[12px] font-extrabold tracking-widest uppercase">{phase.title}</div>
|
||||
<div className="text-[13px] md:text-[14px] leading-relaxed text-zinc-400 group-hover:text-black md:pr-10">{phase.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="border-t border-white/20 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* BOTTOM BRANDING */}
|
||||
<motion.div
|
||||
animate={{ opacity: isAtBottom ? 0 : 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed bottom-0 left-0 w-full pointer-events-none z-50 overflow-hidden pt-10"
|
||||
>
|
||||
<motion.div
|
||||
style={{ padding: bottomPadding }}
|
||||
className="flex justify-between items-end"
|
||||
>
|
||||
<motion.h1 style={{ fontSize: cargoFontSize }} className="font-bebas leading-[0.8] text-black mix-blend-difference tracking-tighter">A.N.T</motion.h1>
|
||||
<motion.h1 style={{ fontSize: archFontSize }} className="font-bebas leading-[0.8] text-black mix-blend-difference tracking-tighter">ARCHITECTURE</motion.h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function ServiceCard({ service, index, scrollYProgress }: { service: any, index: number, scrollYProgress: any }) {
|
||||
// Cards start visible but at different Y positions (staggered cascade)
|
||||
// Card 0: y=0 (top), Card 1: y=200px lower, Card 2: y=400px lower
|
||||
// As user scrolls, each card slides UP to y=0 to align with Card 0
|
||||
const initialOffsets = [0, 200, 400]
|
||||
|
||||
// Sequential timing: Card 1 arrives first, Card 2 arrives after
|
||||
const ranges: [number, number][] = [
|
||||
[0, 0.01], // Card 0: always in place
|
||||
[0, 0.50], // Card 1: slides up during first half
|
||||
[0.15, 0.85], // Card 2: starts after Card 1 is moving, arrives by 85%
|
||||
]
|
||||
|
||||
const y = useTransform(
|
||||
scrollYProgress,
|
||||
ranges[index],
|
||||
[initialOffsets[index], 0]
|
||||
)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
style={{ y }}
|
||||
className="relative w-full h-full bg-white text-black flex flex-col rounded-2xl overflow-hidden"
|
||||
>
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 p-10 lg:p-14 overflow-hidden">
|
||||
<h2 className="text-[32px] lg:text-[40px] mb-10 font-normal tracking-tight leading-[1.1]">
|
||||
{service.title}
|
||||
</h2>
|
||||
<p className="text-[15px] mb-10 text-black/80 leading-relaxed font-medium">
|
||||
{service.description}
|
||||
</p>
|
||||
<ul className="space-y-4 mb-12">
|
||||
{service.list.map((item: string, i: number) => (
|
||||
<li key={i} className="text-[14px] text-black/70 flex items-start border-b border-black/5 pb-2 last:border-0 leading-snug">
|
||||
<span className="mr-3 text-black/30">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{service.button && (
|
||||
<Link href="/projects" className="inline-block border border-black text-[10px] font-bold tracking-[0.2em] px-8 py-3.5 uppercase hover:bg-black hover:text-white transition-all duration-300">
|
||||
{service.button}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fixed Image at Bottom */}
|
||||
<div className="h-[35%] lg:h-[40%] relative w-full overflow-hidden grayscale group hover:grayscale-0 transition-all duration-700">
|
||||
<Image
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
fill
|
||||
sizes="33vw"
|
||||
className="object-cover scale-110 group-hover:scale-100 transition-transform duration-1000"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServiceCardMobile({ service }: { service: any }) {
|
||||
return (
|
||||
<div className="w-full bg-white text-black flex flex-col overflow-hidden rounded-[1px] mb-1">
|
||||
<div className="p-8">
|
||||
<h2 className="text-[26px] mb-6 font-normal tracking-tight leading-tight">{service.title}</h2>
|
||||
<p className="text-[14px] mb-6 text-black/80 leading-relaxed">{service.description}</p>
|
||||
<ul className="space-y-3 mb-8">
|
||||
{service.list.map((item: string, i: number) => (
|
||||
<li key={i} className="text-[13px] text-black/70 flex items-start border-b border-black/5 pb-2 last:border-0">
|
||||
<span className="mr-2 text-black/30">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{service.button && (
|
||||
<Link href="/projects" className="inline-block border border-black text-[9px] font-bold tracking-widest px-6 py-3 uppercase">
|
||||
{service.button}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[250px] relative w-full grayscale">
|
||||
<Image src={service.image} alt={service.title} fill sizes="100vw" className="object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
app/contact/page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
const socialLinks = [
|
||||
{ name: 'FACEBOOK', href: 'https://facebook.com' },
|
||||
{ name: 'INSTAGRAM', href: 'https://instagram.com' },
|
||||
{ name: 'LINKEDIN', href: 'https://linkedin.com' },
|
||||
{ name: '***', href: '**' },
|
||||
]
|
||||
|
||||
const offices = [
|
||||
{
|
||||
num: '01',
|
||||
name: 'Fethiye | Merkez Ofis',
|
||||
address: 'Karagözler, 48300 Fethiye/Muğla',
|
||||
phone: '0553 093 72 25'
|
||||
}
|
||||
]
|
||||
|
||||
export default function ContactPage() {
|
||||
|
||||
return (
|
||||
<main className="relative min-h-screen bg-[#111111] text-white flex flex-col selection:bg-white selection:text-black">
|
||||
|
||||
<div className="flex-1 flex flex-col justify-center items-end px-10 pt-40">
|
||||
<div className="flex flex-wrap justify-end gap-4">
|
||||
{socialLinks.map((link) => (
|
||||
<Link
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className="border border-white/20 px-4 py-2 text-[10px] font-extrabold tracking-widest hover:bg-white hover:text-black transition-all duration-300 rounded-[1px]"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full px-10 pb-40">
|
||||
<div className="border-t border-white/20">
|
||||
{offices.map((office) => (
|
||||
<div
|
||||
key={office.num}
|
||||
className="grid grid-cols-1 md:grid-cols-[60px_1fr_1fr_1fr] gap-8 py-10 border-b border-white/20 items-start hover:bg-white hover:text-black px-6 -mx-6 transition-all duration-300 group cursor-default"
|
||||
>
|
||||
<div className="text-[12px] font-bold text-white/40 group-hover:text-black/40">
|
||||
{office.num}
|
||||
</div>
|
||||
<div className="text-[20px] md:text-[24px] font-normal tracking-tight">
|
||||
{office.name}
|
||||
</div>
|
||||
<div className="text-[12px] leading-relaxed text-zinc-400 group-hover:text-black whitespace-pre-line">
|
||||
{office.address}
|
||||
</div>
|
||||
<div className="text-[12px] font-bold text-right group-hover:text-black">
|
||||
{office.phone}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-bebas: var(--font-bebas-neue);
|
||||
--font-swiss: var(--font-oswald);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -22,5 +24,15 @@
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: var(--font-oswald), Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Bebas_Neue, Oswald } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import LayoutContent from "@/components/LayoutContent";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -12,9 +13,20 @@ const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const bebasNeue = Bebas_Neue({
|
||||
variable: "--font-bebas-neue",
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const oswald = Oswald({
|
||||
variable: "--font-oswald",
|
||||
subsets: ["latin", "latin-ext"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "A.N.T ARCHITECTURE",
|
||||
description: "Ayça Nur Turhan - Mimarlık ve Tasarım",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -24,10 +36,12 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
lang="tr"
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${bebasNeue.variable} ${oswald.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<body className="min-h-full flex flex-col bg-white">
|
||||
<LayoutContent>{children}</LayoutContent>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
155
app/page.tsx
@@ -1,65 +1,108 @@
|
||||
import Image from "next/image";
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import ProjectSlider from '@/components/ProjectSlider'
|
||||
import { projects } from '@/data/projects'
|
||||
|
||||
export default function Home() {
|
||||
const [isRevealed, setIsRevealed] = useState(false)
|
||||
const [isAtCorners, setIsAtCorners] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const reveal = setTimeout(() => setIsRevealed(true), 500)
|
||||
const move = setTimeout(() => setIsAtCorners(true), 2500)
|
||||
|
||||
return () => {
|
||||
clearTimeout(reveal)
|
||||
clearTimeout(move)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const transition = { duration: 1.8, ease: [0.76, 0, 0.24, 1] }
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<main className="relative h-screen w-full bg-white overflow-hidden">
|
||||
|
||||
<div className="absolute inset-0 z-50 pointer-events-none">
|
||||
<div className="relative w-full h-full">
|
||||
|
||||
{/* Main Container for the dynamic text */}
|
||||
<motion.div
|
||||
animate={{
|
||||
top: isAtCorners ? "100%" : "50%",
|
||||
left: isAtCorners ? "0%" : "50%",
|
||||
x: isAtCorners ? "0%" : "-50%",
|
||||
y: isAtCorners ? "-100%" : "-100%",
|
||||
}}
|
||||
transition={transition}
|
||||
className="absolute overflow-hidden"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
{/* The Text Reveal & Cross-fade */}
|
||||
<motion.div
|
||||
initial={{ y: "100%" }}
|
||||
animate={{ y: isRevealed ? "0%" : "100%" }}
|
||||
transition={transition}
|
||||
className="relative"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
{/* AYÇA NUR TURHAN */}
|
||||
<motion.h1
|
||||
animate={{ opacity: isAtCorners ? 0 : 1 }}
|
||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||
className="text-[10vw] md:text-[8vw] font-bebas leading-[0.75] text-black tracking-tighter whitespace-nowrap"
|
||||
>
|
||||
AYÇA NUR TURHAN
|
||||
</motion.h1>
|
||||
|
||||
{/* A.N.T (Cross-fading in) */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isAtCorners ? 1 : 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||
className="absolute inset-0 text-[10vw] md:text-[8vw] font-bebas leading-[0.75] text-black tracking-tighter whitespace-nowrap"
|
||||
>
|
||||
A.N.T
|
||||
</motion.h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* ARCHITECTURE (Remains constant) */}
|
||||
<motion.div
|
||||
animate={{
|
||||
top: isAtCorners ? "100%" : "50%",
|
||||
left: isAtCorners ? "100%" : "50%",
|
||||
x: isAtCorners ? "-100%" : "-50%",
|
||||
y: isAtCorners ? "-100%" : "0%",
|
||||
}}
|
||||
transition={transition}
|
||||
className="absolute overflow-hidden"
|
||||
>
|
||||
<motion.h1
|
||||
initial={{ y: "100%" }}
|
||||
animate={{ y: isRevealed ? "0%" : "100%" }}
|
||||
transition={{ ...transition, delay: 0.1 }}
|
||||
className="text-[10vw] md:text-[8vw] font-bebas leading-[0.75] text-black tracking-tighter whitespace-nowrap"
|
||||
>
|
||||
ARCHITECTURE
|
||||
</motion.h1>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isAtCorners && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.6, duration: 1.2, ease: "easeOut" }}
|
||||
className="h-full flex flex-col justify-center z-10"
|
||||
>
|
||||
<ProjectSlider projects={projects} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
87
app/projects/page.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import { projects } from '@/data/projects'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const { scrollY } = useScroll()
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const windowHeight = window.innerHeight
|
||||
const documentHeight = document.documentElement.scrollHeight
|
||||
const scrollPosition = window.scrollY + windowHeight
|
||||
setIsAtBottom(scrollPosition > documentHeight - 100)
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
const cargoFontSize = useTransform(scrollY, [0, 300], ["10vw", "4vw"])
|
||||
const archFontSize = useTransform(scrollY, [0, 300], ["8vw", "3vw"])
|
||||
const bottomPadding = useTransform(scrollY, [0, 300], ["2.5rem", "1rem"])
|
||||
|
||||
return (
|
||||
<main className="relative min-h-screen bg-white pt-32 pb-60 px-6 md:px-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
{projects.map((project, idx) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: idx * 0.05 }}
|
||||
className="group cursor-pointer"
|
||||
>
|
||||
<div className="relative aspect-[4/3] overflow-hidden rounded-[1px] bg-zinc-100">
|
||||
<Image
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
fill
|
||||
className="object-cover grayscale group-hover:grayscale-0 transition-all duration-700 ease-in-out scale-105 group-hover:scale-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col space-y-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<span className="text-[10px] font-bold text-black/60 uppercase tracking-wider">
|
||||
{project.year} — {project.location}
|
||||
</span>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] font-bold text-black uppercase tracking-widest">
|
||||
{project.title}
|
||||
</span>
|
||||
<span className="text-sm">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
animate={{ opacity: isAtBottom ? 0 : 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed bottom-0 left-0 w-full pointer-events-none z-50 overflow-hidden pt-10"
|
||||
>
|
||||
<motion.div
|
||||
style={{ padding: bottomPadding }}
|
||||
className="flex justify-between items-end"
|
||||
>
|
||||
<motion.h1
|
||||
style={{ fontSize: cargoFontSize }}
|
||||
className="font-bebas leading-[0.8] text-black tracking-tighter"
|
||||
>
|
||||
A.N.T
|
||||
</motion.h1>
|
||||
<motion.h1
|
||||
style={{ fontSize: archFontSize }}
|
||||
className="font-bebas leading-[0.8] text-black tracking-tighter"
|
||||
>
|
||||
ARCHITECTURE
|
||||
</motion.h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
</main>
|
||||
)
|
||||
}
|
||||
35
components/Footer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="w-full bg-[#111111] text-white pt-12 pb-16 px-6 md:px-10 border-t border-white/10">
|
||||
<div className="flex flex-col space-y-12 md:space-y-0 md:flex-row md:justify-between md:items-end">
|
||||
|
||||
{/* Left Side Branding */}
|
||||
<div className="text-[32px] md:text-[20px] font-bebas tracking-tighter leading-none text-center md:text-left">
|
||||
A.N.T
|
||||
</div>
|
||||
|
||||
{/* Center: Copyright & Credits */}
|
||||
<div className="flex flex-col items-center space-y-4 md:space-y-0 md:pb-1">
|
||||
<div className="text-[9px] md:text-[10px] font-extrabold uppercase tracking-[0.15em] text-white/40 text-center leading-relaxed max-w-[300px] md:max-w-none">
|
||||
©A.N.T ARCHITECTURE INC. 2026
|
||||
</div>
|
||||
<div className="text-[9px] md:text-[10px] font-extrabold uppercase tracking-[0.15em] text-white/40 text-center flex flex-wrap justify-center gap-x-2">
|
||||
<span className="hidden md:inline">CREATED BY
|
||||
</span>
|
||||
<Link href="https://ayris.tech" className="hover:text-white transition-colors underline md:no-underline">AYRISTECH</Link>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side Branding */}
|
||||
<div className="text-[32px] md:text-[20px] font-bebas tracking-tighter leading-none text-center md:text-right">
|
||||
ARCHITECTURE
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
20
components/LayoutContent.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const isHome = pathname === "/";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
{!isHome && <Footer />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
150
components/Navbar.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
const navLinks = [
|
||||
{ name: 'ANASAYFA', href: '/' },
|
||||
{ name: 'PROJELER', href: '/projects' },
|
||||
{ name: 'HAKKIMIZDA', href: '/about' },
|
||||
{ name: 'İLETİŞİM', href: '/contact' },
|
||||
]
|
||||
|
||||
// Slot machine style link – text rolls down from top on hover
|
||||
function SlotLink({ href, children, className = '' }: { href: string, children: string, className?: string }) {
|
||||
return (
|
||||
<Link href={href} className={`relative inline-block overflow-hidden group ${className}`}>
|
||||
<span className="block transition-transform duration-300 ease-[cubic-bezier(0.76,0,0.24,1)] group-hover:translate-y-full">
|
||||
{children}
|
||||
</span>
|
||||
<span className="absolute left-0 top-0 block -translate-y-full transition-transform duration-300 ease-[cubic-bezier(0.76,0,0.24,1)] group-hover:translate-y-0" aria-hidden="true">
|
||||
{children}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 80)
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* INITIAL NAVBAR */}
|
||||
<motion.nav
|
||||
initial={{ y: 0, opacity: 1 }}
|
||||
animate={{
|
||||
y: isScrolled ? -100 : 0,
|
||||
opacity: isScrolled ? 0 : 1
|
||||
}}
|
||||
transition={{ duration: 0.5, ease: [0.33, 1, 0.68, 1] }}
|
||||
className="fixed top-0 left-0 w-full flex items-center justify-center px-6 md:px-10 py-6 z-[100] bg-white/80 backdrop-blur-md uppercase text-[12px] font-extrabold tracking-[0.05em] text-black"
|
||||
>
|
||||
{/* Mobile: Logo left + hamburger right */}
|
||||
<div className="flex-1 md:hidden">
|
||||
<Link href="/" className="text-[16px] font-bebas tracking-tighter">A.N.T</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Links – centered */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
{navLinks.map((link) => (
|
||||
<SlotLink key={link.name} href={link.href}>
|
||||
{link.name}
|
||||
</SlotLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="flex flex-col space-y-1.5 p-2"
|
||||
>
|
||||
<span className={`block w-6 h-0.5 bg-black transition-transform ${isMobileMenuOpen ? 'rotate-45 translate-y-2' : ''}`} />
|
||||
<span className={`block w-6 h-0.5 bg-black transition-opacity ${isMobileMenuOpen ? 'opacity-0' : ''}`} />
|
||||
<span className={`block w-6 h-0.5 bg-black transition-transform ${isMobileMenuOpen ? '-rotate-45 -translate-y-2' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</motion.nav>
|
||||
|
||||
{/* SCROLLED COMPACT NAVBAR */}
|
||||
<AnimatePresence>
|
||||
{isScrolled && !isMobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ y: -50, opacity: 0, x: 20 }}
|
||||
animate={{ y: 24, opacity: 1, x: -24 }}
|
||||
exit={{ y: -50, opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.5, ease: [0.33, 1, 0.68, 1] }}
|
||||
className="fixed top-0 right-0 z-[100] flex items-stretch h-12 bg-black text-white rounded-[2px] overflow-hidden shadow-2xl"
|
||||
>
|
||||
<div className="hidden md:flex items-center px-6 space-x-6">
|
||||
{navLinks.map((link) => (
|
||||
<SlotLink
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className="text-[10px] font-extrabold tracking-widest uppercase"
|
||||
>
|
||||
{link.name}
|
||||
</SlotLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Scrolled Mobile Toggle */}
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(true)}
|
||||
className="md:hidden px-4 flex items-center"
|
||||
>
|
||||
<span className="text-[10px] font-extrabold tracking-widest">MENÜ</span>
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* MOBILE OVERLAY MENU */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[150] bg-white flex flex-col p-10"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-20">
|
||||
<Link href="/" onClick={() => setIsMobileMenuOpen(false)} className="text-[24px] font-bebas tracking-tighter">A.N.T</Link>
|
||||
<button onClick={() => setIsMobileMenuOpen(false)} className="text-[12px] font-extrabold tracking-widest">KAPAT</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-8">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="text-[40px] font-bebas tracking-tight text-black hover:pl-4 transition-all duration-300"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-10 border-t border-black/5">
|
||||
<div className="flex items-center space-x-6 text-[12px] font-bold text-black">
|
||||
<button className="underline underline-offset-4">TR</button>
|
||||
<button className="opacity-40">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
76
components/ProjectSlider.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { motion, useAnimationFrame } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import { Project } from '@/data/projects'
|
||||
|
||||
interface ProjectSliderProps {
|
||||
projects: Project[]
|
||||
}
|
||||
|
||||
export default function ProjectSlider({ projects }: ProjectSliderProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Duplicate projects for seamless infinite loop
|
||||
const doubledProjects = [...projects, ...projects]
|
||||
|
||||
// Speed of the automatic scroll
|
||||
const speed = 0.8
|
||||
|
||||
useAnimationFrame(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
containerRef.current.scrollLeft += speed
|
||||
|
||||
const firstHalfWidth = containerRef.current.scrollWidth / 2
|
||||
|
||||
if (containerRef.current.scrollLeft >= firstHalfWidth) {
|
||||
containerRef.current.scrollLeft = 0
|
||||
}
|
||||
})
|
||||
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
if (!containerRef.current) return
|
||||
containerRef.current.scrollLeft += e.deltaY
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onWheel={handleWheel}
|
||||
className="flex-1 flex items-center overflow-x-auto no-scrollbar cursor-grab active:cursor-grabbing py-10"
|
||||
>
|
||||
<div className="flex space-x-6 md:space-x-12 px-6 md:px-10">
|
||||
{doubledProjects.map((project, idx) => (
|
||||
<div
|
||||
key={`${project.id}-${idx}`}
|
||||
className="group relative flex-shrink-0 w-[80vw] sm:w-[500px] md:w-[600px]"
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="relative aspect-[4/3] overflow-hidden rounded-[1px]">
|
||||
<Image
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
fill
|
||||
sizes="(max-width: 768px) 80vw, 600px"
|
||||
className="object-cover grayscale hover:grayscale-0 transition-all duration-700 ease-in-out scale-110 group-hover:scale-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Project Info */}
|
||||
<div className="mt-4 flex justify-between items-start opacity-0 group-hover:opacity-100 transition-opacity duration-500">
|
||||
<div className="text-[10px] md:text-[12px] font-bold text-black tracking-wider uppercase">
|
||||
{project.year} — {project.location}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-[10px] md:text-[12px] font-bold text-black tracking-wider uppercase">
|
||||
<span>{project.title}</span>
|
||||
<span className="text-sm md:text-lg">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
data/projects.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export interface Project {
|
||||
id: number;
|
||||
year: string;
|
||||
location: string;
|
||||
title: string;
|
||||
image: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export const projects: Project[] = [
|
||||
{
|
||||
id: 1,
|
||||
year: '2018',
|
||||
location: 'SAINT-AUGUSTIN-DE-DESMAURES',
|
||||
title: 'JDHM – GENEL MERKEZ',
|
||||
image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Ticari'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
year: '2019',
|
||||
location: 'MONTREAL, QC',
|
||||
title: 'ŞALE',
|
||||
image: 'https://images.unsplash.com/photo-1518780664697-55e3ad937233?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Konut'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
year: '2020',
|
||||
location: 'QUEBEC CITY, QC',
|
||||
title: 'MODERN MÜZE',
|
||||
image: 'https://images.unsplash.com/photo-1511818966892-d7d671e672a2?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Kültürel'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
year: '2021',
|
||||
location: 'TORONTO, ON',
|
||||
title: 'ŞEHİR KULESİ',
|
||||
image: 'https://images.unsplash.com/photo-1449156001533-cb39414bb589?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Konut'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
year: '2022',
|
||||
location: 'VANCOUVER, BC',
|
||||
title: 'ORMAN EVİ',
|
||||
image: 'https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Konut'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
year: '2023',
|
||||
location: 'SAINTE-FOY, QC',
|
||||
title: 'KOCINA',
|
||||
image: 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Ticari'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
year: '2022',
|
||||
location: 'MONT-TREMBLANT, QC',
|
||||
title: 'YAMAÇ EVİ',
|
||||
image: 'https://images.unsplash.com/photo-1497366216548-37526070297c?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Konut'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
year: '2021',
|
||||
location: 'LÉVIS, QC',
|
||||
title: 'MAKUSHAM STÜDYO',
|
||||
image: 'https://images.unsplash.com/photo-1431540015161-0bf868a2d407?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Kültürel'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
year: '2023',
|
||||
location: 'BROSSARD, QC',
|
||||
title: 'MUST SOCIÉTÉ',
|
||||
image: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Ticari'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
year: '2022',
|
||||
location: 'RIMOUSKI, QC',
|
||||
title: 'GÖL KENARI KULÜBESİ',
|
||||
image: 'https://images.unsplash.com/photo-1449156001533-cb39414bb589?q=80&w=2070&auto=format&fit=crop',
|
||||
category: 'Konut'
|
||||
}
|
||||
];
|
||||
0
docs/prd.md
Normal file
@@ -1,7 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
43
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "aycanur",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.38.0",
|
||||
"next": "16.2.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
@@ -3692,6 +3693,33 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.38.0",
|
||||
"motion-utils": "^12.36.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -5019,6 +5047,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.36.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.36.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.38.0",
|
||||
"next": "16.2.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
|
||||
BIN
public/font/Swiss721Black.ttf
Normal file
BIN
public/font/Swiss721Bold.ttf
Normal file
BIN
screenshots/1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
screenshots/10.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
screenshots/11.png
Normal file
|
After Width: | Height: | Size: 726 KiB |
BIN
screenshots/12.png
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
screenshots/2.png
Normal file
|
After Width: | Height: | Size: 962 KiB |
BIN
screenshots/3.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
screenshots/4.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
screenshots/5.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
screenshots/6.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
screenshots/7.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
screenshots/8.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
screenshots/9.png
Normal file
|
After Width: | Height: | Size: 246 KiB |