Compare commits

3 Commits
dev ... stable

Author SHA1 Message Date
de47121b39 gitignore: .git-credentials (local push auth)
All checks were successful
Deploy stable / deploy (push) Successful in 20s
2026-04-23 16:01:20 +03:00
8b480c036c Initial impact.tr site
All checks were successful
Deploy stable / deploy (push) Successful in 1m36s
V3 landing: white + violet + yellow accent, bold editorial.
Markdown-driven content: 3 yarışma formatı (hands-on, ideathon, robo-soccer),
5 edisyon (Ataköy, Çemberlitaş Ideathon '25, Çemberlitaş HoC '26, İstanbul/Ankara Robo Soccer).
Sayfalar: anasayfa, yarışma/edisyon detayları, sponsor, hakkımızda, iletişim.
40 Çemberlitaş fotoğrafı (1600px optimize, ~6.4 MB).
Content helper (gray-matter + marked), reviews/photos registry,
iletişim/ekip tek kaynak lib/contact.ts.
Next.js 16 standalone build, Docker + compose hazır.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:35:09 +03:00
97edf69ab3 landing: impact placeholder
All checks were successful
Deploy stable / deploy (push) Successful in 29s
2026-04-23 10:35:13 +03:00
75 changed files with 2813 additions and 97 deletions

3
.gitignore vendored
View File

@@ -24,6 +24,9 @@
.DS_Store
*.pem
# git credentials (local push auth only — never commit)
.git-credentials
# debug
npm-debug.log*
yarn-debug.log*

179
README.md
View File

@@ -1,36 +1,173 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# impact.tr
## Getting Started
Next.js 16 + Tailwind 4. SSG, markdown-driven content, self-host (Docker).
First, run the development server:
## Dev / build
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
npm install
npm run dev # localhost:3000 (we use 3010 in the background)
npm run build # production build
npm run start # serve the production build
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Docker:
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
```bash
docker compose up -d --build # HOST_PORT=3010 by default
```
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Content workflow (no CMS, yalnızca markdown + git)
## Learn More
Yeni edisyon, yeni yarışma formatı, yeni foto = dosya ekle + commit + push. Site otomatik güncellenir.
To learn more about Next.js, take a look at the following resources:
### Yeni edisyon ekle
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
1. `content/editions/<slug>.md` dosyası oluştur. Örnek:
```md
---
slug: ankara-hands-on-2026
competition: hands-on-challenge # hands-on-challenge | ideathon | robo-soccer
name: Hands-On Challenge — Ankara Regional
status: past # past | upcoming | announced
startDate: "2026-06-14"
endDate: "2026-06-15"
city: Ankara
venue: ODTÜ KKM
stats:
teams: 24
participants: 200
volunteers: 25
winners:
- { place: 1, team: "Takım Adı", school: "Okul", members: 4 }
- { place: 2, team: "...", school: "...", members: 3 }
teams:
- { name: "Takım A", school: "Lise X", size: 4 }
- { name: "Takım B", school: "Ü. Y", size: 3 }
partners:
- "Partner 1"
testimonials:
- { by: "İsim", org: "Kurum", quote: "..." }
summary: "Tek cümlelik özet, kart & meta yerlerinde görünür."
---
Serbest metin (markdown). `##`, `###`, listeler, bağlantılar.
```
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
2. Foto klasörü oluştur (opsiyonel): `public/photos/<slug>/01.jpg` … `NN.jpg`.
Şu an otomatik galeri **sadece** `cemberlitas-hands-on-2026` için bağlı — yeni edisyonları otomatik çekmesi için tek satırlık helper gerekiyor (aşağıda TODO).
## Deploy on Vercel
3. Commit + push → SSG yeniden üretir, anasayfadaki "Edisyonlar" grid'i ve toplam metrikler (`stats` varsa) otomatik güncellenir.
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
### Yeni yarışma formatı
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
`content/competitions/<slug>.md`:
```md
---
slug: archeology-hackathon
name: Archeology Hackathon Challenge
short: Kültürel miras hackathonu
tagline: "Bir cümle hook."
format: "Kısa açıklama."
durationDays: 2
ageGroups: [Lise, Üniversite]
categories: [Veri, Görüntü işleme]
color: brand # brand | accent | pink
order: 4
---
Markdown body.
```
Sonra `src/components/Nav.tsx`'a bir link daha ekle; ve `src/lib/content.ts`'te `CompetitionSlug` union type'ına yeni slug'ı ekle.
### Yeni yorum / review
`src/lib/reviews.ts` içindeki `reviews` dizisine ekle:
```ts
{
quote: "...",
by: "İsim",
org: "Kurum",
edition: "Çemberlitaş 2026",
}
```
Edition-specific testimonial'lar için o edisyonun `.md` dosyasındaki `testimonials` alanını kullan — edisyon detay sayfasında görünür.
### Foto ekle
- Yeni foto: `public/photos/<klasör>/<ad>.jpg`. 1600px genişlik civarı, JPG quality ~78 öneri.
- Çoklu indirme için Drive linki varsa:
```bash
/tmp/gdown-venv/bin/gdown --folder "<drive-folder-url>" -O public/photos/<klasör>
# sonra Pillow ile 1600px'e küçült (script örneği geliştirilebilir)
```
## Pending / TODO
Kod seviyesinde hazır olup **veri bekleyen** şeyler:
- [ ] **Ataköy edisyonu** — tarih (`startDate`/`endDate`) doğrulanmalı. Şu an `2025-12-20/21` tahmin. Takım listesi + kazananlar eklenmeli.
- [ ] **Çemberlitaş Hands-On 2026** — 16 takımın tam listesi (okul + kişi) `teams` alanına girilmeli. Tüm podium (2. ve 3.) girilmeli.
- [ ] **Çemberlitaş Ideathon 2025** — stats (katılımcı/takım sayısı), kazananlar, varsa foto klasörü.
- [ ] **Robo Soccer** — resmi isim kesinleşirse (`Robo Soccer` mı `Impact Robo Soccer` mı), tarih/mekan netleşince `istanbul-robo-soccer.md` ve `ankara-robo-soccer.md` güncellenmeli. Başvuru Google Forms URL'si ekle → yarışma sayfasına embed.
- [ ] **Ataköy foto klasörü** — Drive'da `1JzYEOfWaT6oTSDgycG0GCg-rXrlZRK5K` çekilemedi (sadece boş alt klasörler indirdi). Manuel indirip `public/photos/atakoy-hands-on/` altına kopyalanmalı.
- [ ] **Ek testimonials** — şu an yalnızca M. Buğrahan Kılıç'ın yorumu var. Katılımcı/gönüllü yorumları toplanmalı.
- [ ] **Diğer etkinlikler (LinkedIn'den çıkan)** — ZAĞANOS International STEM Expo, ZAĞANOS Case Study Challenge, Archeology Hackathon Challenge, Arduino workshops. Bunların hangisi ayrı bir format mı, hangisi tek seferlik mi karar ver; `content/competitions/` veya `content/editions/` altına uygun şekilde eklenmeli.
Kod seviyesinde **yapılacaklar**:
- [ ] Otomatik edisyon galerisi: `public/photos/<edition-slug>/` klasörü varsa edisyon detay sayfasına otomatik `<Gallery>` düşsün. Şu an sadece `cemberlitas-hands-on-2026` için hardcoded. `src/lib/photos.ts` gibi bir helper: `getEditionPhotos(slug)` → `fs.readdirSync` ile otomatik.
- [ ] **OG image** (`src/app/opengraph-image.tsx`) — WhatsApp/Slack/Twitter link preview için.
- [ ] **sitemap.xml** ve **robots.txt** (`src/app/sitemap.ts`, `src/app/robots.ts`).
- [ ] **Google Forms embed** Robo Soccer başvuru için. Form URL'si geldiğinde `/yarismalar/robo-soccer/basvuru` route'u ya da yarışma sayfasına iframe blok.
- [ ] **Yorum gönderme** — şu an `mailto:` linki. İleride bir Google Forms ile Sheets'e düşsün; yeni yorumları elden `reviews.ts`'ye taşıma akışı.
- [ ] **"Önceki etkinlikler" arşiv sayfası** — `/edisyonlar` index'i henüz yok; ana sayfadan 6 kart görünüyor. Tüm edisyonları listeleyen bir sayfa eklenmeli.
- [ ] **Gerçek logo** — şu an "i" harfi violet blok. IG'deki "impact" wordmark SVG'ye çevirilip `public/logo.svg` + `Logo.tsx` güncellenmeli.
- [ ] **Foto optimizasyonu** — şu an 1600px JPG. Next Image zaten optimize ediyor ama AVIF/WebP export eklenebilir.
- [ ] **E-posta rate-limiting / iletişim formu** — mailto iyi ama spam gelirse form'a taşıyabiliriz.
## Deploy
Self-host Docker Compose. Üretim adımları:
```bash
# Reverse proxy (caddy/nginx) impact.tr → 127.0.0.1:3010
docker compose up -d --build
docker compose logs -f web
```
Reverse proxy örneği (Caddy):
```caddy
impact.tr {
reverse_proxy 127.0.0.1:3010
}
```
## Yapı
```
content/
competitions/*.md # yarışma formatları
editions/*.md # edisyonlar (metrik, kazanan, takım, testimonial)
public/
photos/<slug>/*.jpg # edisyon/format fotoğrafları
src/
app/ # App Router sayfaları
page.tsx # anasayfa
yarismalar/[slug]/ # yarışma detay
edisyonlar/[slug]/ # edisyon detay
sponsor/, hakkimizda/, iletisim/
components/ # Nav, Footer, Section, PageHero, Gallery, PhotoStrip, ...
lib/
content.ts # markdown loader, helpers
contact.ts # iletişim, ekip, değerler, SDG (tek kaynak)
reviews.ts # yorumlar + foto index
```
## İletişim
- Site: [impact.tr](https://impact.tr)
- E-posta: contact@impact.tr · info@impact.tr · hello@impact.tr
- Ekip: tarik@ · tuna@ · yigit@ impact.tr
- Instagram: [@impact.tr](https://www.instagram.com/impact.tr/)
- LinkedIn: [impactcommunit](https://www.linkedin.com/company/impactcommunit/)

View File

@@ -0,0 +1,33 @@
---
slug: hands-on-challenge
name: Hands-On Challenge
short: Robotik yarışması
tagline: "ESP32 ile takım bazlı, non-stop robotik üretim maratonu."
format: "1-2 gün boyunca takımlar; mekanik tasarım, elektronik ve yazılımı birleştirerek göreve özel robotlar geliştirir ve sahada yarıştırır."
durationDays: 2
ageGroups:
- Lise
- Üniversite
categories:
- Robotik tasarım
- Gömülü yazılım
- Mekatronik
color: brand
icon: robot
order: 1
---
## Format
Hands-On Challenge, Impact'in amiral yarışmasıdır. Takımlar belirlenen göreve ilişkin bir robotu tasarlar, kod yazar, sahada jüri karşısında test eder.
- **Süre:** 1-2 gün non-stop üretim
- **Takım:** 3-5 kişi
- **Platform:** ESP32 odaklıık platform
- **Puanlama:** Görev performansı + jüri değerlendirmesi
## Neden farklı
- Türkiye'de **ESP32'ye özel** ilk ulusal format
- 150 TL kit erişilebilirliği ile **sosyoekonomik kapsayıcılık**
- Regional → championship funnel'i

View File

@@ -0,0 +1,32 @@
---
slug: ideathon
name: Ideathon
short: Fikir maratonu
tagline: "Bir günde fikirden projeye: tema, mentor, jüri."
format: "Tek günlük fikir maratonu. Açılış seminerleri ile başlar, temanın duyurulmasının ardından takımlar mentor desteğiyle fikir üretir ve günün sonunda jüri karşısında sunum yapar."
durationDays: 1
ageGroups:
- Lise
categories:
- Sürdürülebilirlik
- Sosyal inovasyon
- Teknoloji
color: accent
icon: lightbulb
order: 2
---
## Format
- **Süre:** 11 saat (10:00 21:00)
- **Takım:** 3-5 öğrenci
- **Akış:** Açılış semineri → tema reveal → mentorlu fikir üretimi → ara atölyeler → jüri sunumu + ödül töreni
- **Katılım:** Ücretsiz
## Çıktı
Her takım, günün sonunda:
- Problem tanımı
- Çözüm konsepti
- 5 dakikalık pitch sunumu
- Prototip / mockup

View File

@@ -0,0 +1,33 @@
---
slug: robo-soccer
name: Robo Soccer
short: Robot futbol ligi
tagline: "5 şehirde regional, ardından büyük final: Türkiye'nin yeni robot ligi."
format: "Takımlar otonom robotlarıyla sahada karşılaşır. Sezon boyunca 5 regional düzenlenir; en başarılı takımlar Championship'e kalır."
durationDays: 1
ageGroups:
- Lise
- Üniversite
categories:
- Otonom kontrol
- Görüntü işleme
- Mekatronik
color: accent
icon: ball
order: 3
---
## Sezon Yapısı
1. **5 Regional** — her şehirde elemeler. İstanbul ve Ankara ile başlıyoruz; kalan üç şehir yakında açıklanacak.
2. **Championship** — regional'lardan gelen top takımlar tek elemeli büyük finalde karşılaşır.
## Takım
- 3-5 kişilik takım
- Robotu kendin getir (BYOR) — standartlar sezon başında duyurulur
- Her regional'a ayrı başvuru açılır
## Duyuru
Tarihler ve başvuru için WhatsApp topluluğumuza katıl, Instagram'ı takip et.

View File

@@ -0,0 +1,10 @@
---
slug: ankara-robo-soccer
competition: robo-soccer
name: Robo Soccer — Ankara Regional
status: announced
city: Ankara
summary: "Sezonun ikinci durağı Ankara'da. Tarih ve mekan yakında."
---
Sezonun ikinci durağı Ankara'da. Detaylar yakında duyurulacak.

View File

@@ -0,0 +1,17 @@
---
slug: atakoy-hands-on
competition: hands-on-challenge
name: Hands-On Challenge — Istanbul Regional (Ataköy)
status: past
city: İstanbul
venue: Ataköy Gençlik Merkezi
stats:
teams: 16
participants: 100
volunteers: 15
summary: "Impact'in ilk bölgesel Hands-On Challenge edisyonu. Ataköy Gençlik Merkezi'nde lise ve üniversite öğrencileri iki gün süren üretim maratonunda bir araya geldi."
---
İlk bölgesel edisyonumuz Ataköy Gençlik Merkezi'nde düzenlendi. 16 takım, toplam 100'ü aşkın katılımcı ve 15 gönüllüyle, Impact'in format prensiplerini sahaya indirdiğimiz ilk büyük organizasyon oldu.
Tarih, detaylı takım listesi, kazananlar ve galeri yakında eklenecek.

View File

@@ -0,0 +1,28 @@
---
slug: cemberlitas-hands-on-2026
competition: hands-on-challenge
name: Hands-On Challenge — Çemberlitaş Regional
status: past
startDate: "2026-04-18"
endDate: "2026-04-19"
city: İstanbul
venue: Çemberlitaş Gençlik Merkezi
stats:
teams: 16
participants: 150
volunteers: 20
winners:
- place: 1
team: İTÜ MTAL
school: İTÜ Mesleki ve Teknik Anadolu Lisesi
members: 3
testimonials:
- by: M. Buğrahan Kılıç
org: Sultangazi Bilim ve İnovasyon Merkezi
quote: "Impact Hands-On Challenge Çemberlitaş Regional 2 gün boyunca 16 robotik takımına, 150'den fazla katılımcıya, mentörlere ve eğitimcilere kapılarını açtı. Impact, eğitim programları ve yarışmalarıyla Türkiye'de STEM ekosisteminin her geçen gün gelişmesini sağlıyor. İkinci kez katıldığımız ve keyif aldığımız bu organizasyonu düzenleyen ekibi kutlarız."
summary: "İstanbul'un en büyük robot yarışmalarından biri. 2 günlük non-stop üretim maratonu."
---
18-19 Nisan 2026'da Çemberlitaş Gençlik Merkezi'nde gerçekleştirdiğimiz edisyon, İstanbul'un en büyük robot yarışmalarından biri oldu. Ekipler tek günde verilen görevlere ilişkin robotlar geliştirdi; iki günlük non-stop üretim maratonu ve problem çözme odaklı bir robotik deneyimi yaşandı.
> Detaylı takım listesi ve tüm kazananlar yakında eklenecek.

View File

@@ -0,0 +1,17 @@
---
slug: cemberlitas-ideathon-2025
competition: ideathon
name: Ideathon — Çemberlitaş Regional
status: past
startDate: "2025-10-05"
endDate: "2025-10-05"
city: İstanbul
venue: Çemberlitaş Gençlik Merkezi
partners:
- Gençlik ve Spor Bakanlığı
summary: "11 saatlik fikir maratonu. Lise öğrencileri 3-5 kişilik takımlar halinde mentorlu fikir üretimi yaptı."
---
10:00'da açılış seminerleri ile başlayan program, tema duyurusunun ardından mentorlu çalışma saatleriyle devam etti. Gün sonunda takımlar jüri karşısında sunumlarını yaptı, ödül töreni ile kapanış gerçekleşti.
> Takım isimleri ve kazananlar yakında eklenecek.

View File

@@ -0,0 +1,10 @@
---
slug: istanbul-robo-soccer
competition: robo-soccer
name: Robo Soccer — İstanbul Regional
status: upcoming
city: İstanbul
summary: "Robo Soccer sezonunun ilk durağı. Tarih ve mekan yakında açıklanacak."
---
Impact Robo Soccer sezonunun ilk durağı İstanbul'da. Tarih, mekan ve başvuru detayları yakında duyurulacak. WhatsApp topluluğumuza katılarak ilk haberi alanlardan ol.

122
package-lock.json generated
View File

@@ -8,6 +8,8 @@
"name": "repo",
"version": "0.1.0",
"dependencies": {
"gray-matter": "^4.0.3",
"marked": "^18.0.2",
"next": "16.2.4",
"react": "19.2.4",
"react-dom": "19.2.4"
@@ -3423,6 +3425,19 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -3469,6 +3484,18 @@
"node": ">=0.10.0"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3804,6 +3831,43 @@
"dev": true,
"license": "ISC"
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -4125,6 +4189,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4517,6 +4590,15 @@
"json-buffer": "3.0.1"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -4868,6 +4950,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "18.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz",
"integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5634,6 +5728,19 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -5859,6 +5966,12 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -6003,6 +6116,15 @@
"node": ">=4"
}
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",

View File

@@ -9,6 +9,8 @@
"lint": "eslint"
},
"dependencies": {
"gray-matter": "^4.0.3",
"marked": "^18.0.2",
"next": "16.2.4",
"react": "19.2.4",
"react-dom": "19.2.4"

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -0,0 +1,285 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import Image from "next/image";
import { PageHero } from "@/components/PageHero";
import { PhotoStrip } from "@/components/PhotoStrip";
import { Section } from "@/components/Section";
import { StatsStrip } from "@/components/StatsStrip";
import { TestimonialCard } from "@/components/TestimonialCard";
import { Gallery } from "@/components/Gallery";
import {
formatDateRange,
getCompetition,
getEdition,
getEditions,
} from "@/lib/content";
import { photos } from "@/lib/reviews";
export function generateStaticParams() {
return getEditions().map((e) => ({ slug: e.slug }));
}
export async function generateMetadata(
props: PageProps<"/edisyonlar/[slug]">,
): Promise<Metadata> {
const { slug } = await props.params;
const e = getEdition(slug);
if (!e) return {};
return {
title: e.name,
description: e.summary ?? undefined,
};
}
const statusLabel = {
upcoming: "Yaklaşan",
announced: "Duyuruldu",
past: "Geçmiş",
} as const;
export default async function EditionPage(
props: PageProps<"/edisyonlar/[slug]">,
) {
const { slug } = await props.params;
const e = getEdition(slug);
if (!e) notFound();
const c = getCompetition(e.competition);
const isCemberlitasHoC = e.slug === "cemberlitas-hands-on-2026";
const heroPhoto = isCemberlitasHoC ? photos.cemberlitas[14] : undefined;
const gallery = isCemberlitasHoC
? [0, 7, 14, 20, 26, 32].map((i) => photos.cemberlitas[i])
: [];
const strip = isCemberlitasHoC
? [3, 18, 24, 35].map((i) => photos.cemberlitas[i])
: [];
return (
<>
<PageHero
eyebrow={c ? c.name : "Edisyon"}
title={e.name}
lead={e.summary}
variant={heroPhoto ? "photo" : "solid"}
photo={heroPhoto ? { src: heroPhoto.src, alt: e.name } : undefined}
tags={
<>
<span
className={`border border-white/50 px-3 py-1 text-xs font-black uppercase tracking-[0.2em] ${
e.status === "past"
? "bg-white/10 text-white"
: "bg-yellow-300 text-neutral-900"
}`}
>
{statusLabel[e.status]}
</span>
<span className="bg-white/10 px-3 py-1 text-xs font-black uppercase tracking-[0.2em] text-white">
{formatDateRange(e.startDate, e.endDate)}
</span>
{e.city && (
<span className="bg-white/10 px-3 py-1 text-xs font-black uppercase tracking-[0.2em] text-white">
{e.city}
</span>
)}
</>
}
/>
{/* Meta row */}
<section className="border-b-2 border-neutral-900 bg-white">
<div className="mx-auto grid max-w-[1400px] grid-cols-2 gap-8 px-6 py-10 md:grid-cols-4 md:px-10">
<Meta label="Tarih" value={formatDateRange(e.startDate, e.endDate)} />
{e.venue && <Meta label="Mekan" value={e.venue} />}
{e.city && <Meta label="Şehir" value={e.city} />}
{c && <Meta label="Format" value={c.name} />}
</div>
</section>
{/* Stats */}
{e.stats && (
<Section>
<StatsStrip
items={[
...(e.stats.teams != null
? [{ value: e.stats.teams, label: "Takım" }]
: []),
...(e.stats.participants != null
? [{ value: `${e.stats.participants}+`, label: "Katılımcı" }]
: []),
...(e.stats.volunteers != null
? [{ value: e.stats.volunteers, label: "Gönüllü" }]
: []),
...(e.partners && e.partners.length > 0
? [{ value: e.partners.length, label: "Partner" }]
: e.startDate && e.endDate
? [
{
value: String(
Math.round(
(new Date(e.endDate).getTime() -
new Date(e.startDate).getTime()) /
86400000,
) + 1,
),
label: "Gün",
},
]
: []),
]}
/>
</Section>
)}
{/* Body */}
{(e.bodyHtml || e.summary) && (
<Section tone="muted">
<div className="grid gap-10 md:grid-cols-12">
<div className="md:col-span-7">
<div className="prose-impact">
{e.summary && (
<p className="text-xl font-semibold text-neutral-900">{e.summary}</p>
)}
{e.bodyHtml && <div dangerouslySetInnerHTML={{ __html: e.bodyHtml }} />}
</div>
</div>
{heroPhoto && (
<div className="md:col-span-5">
<div className="relative aspect-[4/5] border-2 border-neutral-900 bg-neutral-200">
<Image
src={photos.cemberlitas[11].src}
alt=""
fill
sizes="(min-width: 768px) 40vw, 100vw"
className="object-cover"
/>
</div>
</div>
)}
</div>
</Section>
)}
{/* Gallery */}
{gallery.length > 0 && (
<Section eyebrow="Galeri" title="Sahneden anlar">
<Gallery photos={gallery} />
<div className="mt-6">
<PhotoStrip photos={strip} />
</div>
</Section>
)}
{/* Winners */}
{e.winners && e.winners.length > 0 && (
<Section eyebrow="Kazananlar" title="Podium">
<div className="grid gap-4 md:grid-cols-3">
{e.winners.map((w) => (
<div
key={`${w.place}-${w.team}`}
className="border-2 border-neutral-900 bg-white p-6"
>
<div className="text-xs font-black uppercase tracking-[0.25em] text-violet-700">
{w.place === 1
? "🥇 Birincilik"
: w.place === 2
? "🥈 İkincilik"
: w.place === 3
? "🥉 Üçüncülük"
: `#${w.place}`}
</div>
<div className="mt-3 text-2xl font-black tracking-tight">{w.team}</div>
{w.school && (
<div className="mt-2 text-sm text-neutral-600">{w.school}</div>
)}
{w.members && (
<div className="mt-1 text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
{w.members} kişilik takım
</div>
)}
</div>
))}
</div>
</Section>
)}
{/* Teams */}
{e.teams && e.teams.length > 0 && (
<Section eyebrow="Takımlar" title={`${e.teams.length} takım sahaya çıktı`}>
<div className="overflow-hidden border-2 border-neutral-900">
<table className="w-full text-left text-sm">
<thead className="bg-neutral-900 text-xs font-black uppercase tracking-[0.2em] text-white">
<tr>
<th className="px-5 py-3">Takım</th>
<th className="px-5 py-3">Okul</th>
<th className="px-5 py-3">Kişi</th>
</tr>
</thead>
<tbody className="divide-y-2 divide-neutral-900">
{e.teams.map((t, i) => (
<tr key={`${t.name}-${i}`} className="bg-white">
<td className="px-5 py-3 font-black">{t.name}</td>
<td className="px-5 py-3 text-neutral-700">{t.school ?? "—"}</td>
<td className="px-5 py-3 text-neutral-700">{t.size ?? "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
</Section>
)}
{/* Partners */}
{e.partners && e.partners.length > 0 && (
<Section eyebrow="Partnerler" title="Birlikte gerçekleştirdik">
<div className="flex flex-wrap gap-2">
{e.partners.map((p) => (
<span
key={p}
className="border-2 border-neutral-900 bg-white px-4 py-2 text-sm font-black uppercase tracking-wider"
>
{p}
</span>
))}
</div>
</Section>
)}
{/* Testimonials */}
{e.testimonials && e.testimonials.length > 0 && (
<Section
tone="muted"
eyebrow="Geri dönüşler"
title="Katılımcı ve partner yorumları"
>
<div className="grid gap-5 md:grid-cols-2">
{e.testimonials.map((t, i) => (
<TestimonialCard key={i} t={t} featured={i === 0} />
))}
</div>
</Section>
)}
{/* Back */}
<Section>
<Link
href={c ? `/yarismalar/${c.slug}` : "/"}
className="inline-flex items-center gap-2 text-sm font-black uppercase tracking-[0.2em] text-neutral-900 hover:text-violet-700"
>
{c ? `${c.name} tüm edisyonları` : "Anasayfa"}
</Link>
</Section>
</>
);
}
function Meta({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-[10px] font-black uppercase tracking-[0.25em] text-violet-700">
{label}
</dt>
<dd className="mt-1 text-base font-black">{value}</dd>
</div>
);
}

View File

@@ -1,26 +1,93 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
@theme {
--color-ink: #0a0a0a;
--color-ink-muted: #525252;
--color-ink-faint: #8a8a8a;
--color-paper: #ffffff;
--color-paper-2: #fafafa;
--color-paper-3: #f4f4f5;
--color-line: #0a0a0a;
--color-brand: #6d28d9;
--color-brand-600: #5b21b6;
--color-brand-400: #8b5cf6;
--color-brand-50: #f5f3ff;
--color-accent: #fde047;
--color-accent-600: #facc15;
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
:root {
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
html, body {
background: var(--color-paper);
color: var(--color-ink);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
::selection {
background: var(--color-accent);
color: var(--color-ink);
}
/* Prose tweaks for markdown body (light theme) */
.prose-impact h2 {
font-size: 1.75rem;
font-weight: 800;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
}
.prose-impact h3 {
font-size: 1.25rem;
font-weight: 700;
margin-top: 1.75rem;
margin-bottom: 0.5rem;
}
.prose-impact p {
margin-top: 0.85rem;
margin-bottom: 0.85rem;
color: var(--color-ink-muted);
line-height: 1.7;
}
.prose-impact ul {
list-style: disc;
padding-left: 1.25rem;
color: var(--color-ink-muted);
}
.prose-impact li {
margin-top: 0.25rem;
}
.prose-impact a {
color: var(--color-brand);
text-decoration: underline;
text-underline-offset: 3px;
}
.prose-impact strong {
color: var(--color-ink);
font-weight: 700;
}
/* Marquee (no JS, pure CSS) */
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.marquee-track {
display: flex;
width: max-content;
animation: marquee 40s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.marquee-track { animation: none; }
}

131
src/app/hakkimizda/page.tsx Normal file
View File

@@ -0,0 +1,131 @@
import type { Metadata } from "next";
import Image from "next/image";
import { PageHero } from "@/components/PageHero";
import { PhotoStrip } from "@/components/PhotoStrip";
import { Section } from "@/components/Section";
import { sdgs, site, team, values } from "@/lib/contact";
import { photos } from "@/lib/reviews";
export const metadata: Metadata = {
title: "Hakkımızda",
description: site.description,
};
export default function AboutPage() {
const heroPhoto = photos.cemberlitas[18];
const sidePhotos = [11, 27, 33].map((i) => photos.cemberlitas[i]);
const strip = [4, 16, 22, 39].map((i) => photos.cemberlitas[i]);
return (
<>
<PageHero
eyebrow="Hakkımızda"
title={<>STEM için, gençler tarafından</>}
lead="Impact; robotik yarışmaları, ideathonlar ve eğitim programlarıyla Türkiye'de STEM ekosisteminin her geçen gün gelişmesini sağlayan bir gençlik topluluğu."
variant="photo"
photo={{ src: heroPhoto.src, alt: "Impact etkinlikten bir kare" }}
/>
{/* MISSION */}
<Section eyebrow="Misyon" title="Hedefimiz">
<div className="grid gap-10 md:grid-cols-12">
<div className="md:col-span-7">
<p className="text-xl leading-relaxed text-neutral-800">
Bilim, teknoloji, mühendislik ve matematiği{" "}
<strong className="font-black">herkes için erişilebilir</strong> kılmak;
ilham veren bir gelecek tasarlamak ve gençleri 21. yüzyılın becerileriyle
buluşturmak.
</p>
<p className="mt-6 text-lg leading-relaxed text-neutral-700">
Her etkinliğimiz, geleceği sadece anlayan değil aktif olarak inşa eden bir
nesil yetiştirmeyi hedefler. Gençleri eğitim, sosyal etki ve sürdürülebilir
kalkınmanın kesişiminde gerçek dünya teknolojileriyle tanıştırıyoruz.
</p>
</div>
<div className="grid grid-cols-3 gap-3 md:col-span-5">
{sidePhotos.map((p) => (
<div
key={p.src}
className="relative aspect-[3/4] overflow-hidden border-2 border-neutral-900 bg-neutral-200"
>
<Image
src={p.src}
alt=""
fill
sizes="(min-width: 768px) 15vw, 33vw"
className="object-cover"
/>
</div>
))}
</div>
</div>
</Section>
{/* TEAM */}
<Section tone="muted" eyebrow="Ekip" title="Kurucu ekip">
<div className="grid gap-5 md:grid-cols-3">
{team.map((m) => (
<div
key={m.name}
className="flex flex-col justify-between gap-8 border-2 border-neutral-900 bg-white p-8"
>
<div>
<span className="inline-flex size-14 items-center justify-center bg-violet-700 text-base font-black text-white">
{m.name
.split(" ")
.map((w) => w[0])
.slice(0, 2)
.join("")}
</span>
<div className="mt-6 text-2xl font-black leading-tight tracking-tight">
{m.name}
</div>
<div className="mt-2 text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
{m.role}
</div>
</div>
</div>
))}
</div>
</Section>
{/* PHOTO STRIP */}
<section className="border-b-2 border-neutral-900 bg-white">
<div className="mx-auto max-w-[1400px] px-6 py-14 md:px-10">
<PhotoStrip photos={strip} />
</div>
</section>
{/* VALUES */}
<Section eyebrow="Değerler" title="Bizi yönlendiren altı ilke">
<div className="flex flex-wrap gap-2">
{values.map((v) => (
<span
key={v}
className="border-2 border-neutral-900 bg-white px-5 py-2.5 text-sm font-black uppercase tracking-[0.2em]"
>
{v}
</span>
))}
</div>
</Section>
{/* SDG */}
<Section tone="dark" eyebrow="Sürdürülebilir kalkınma" title="Desteklediğimiz BM SDG'leri">
<div className="grid gap-4 md:grid-cols-4">
{sdgs.map((s) => (
<div
key={s.number}
className="border-2 border-white/30 bg-white/5 p-6"
>
<div className="text-5xl font-black text-yellow-300">{s.number}</div>
<div className="mt-4 text-sm font-black uppercase tracking-[0.15em]">
{s.name}
</div>
</div>
))}
</div>
</Section>
</>
);
}

136
src/app/iletisim/page.tsx Normal file
View File

@@ -0,0 +1,136 @@
import type { Metadata } from "next";
import { PageHero } from "@/components/PageHero";
import { Section } from "@/components/Section";
import { contact, team } from "@/lib/contact";
export const metadata: Metadata = {
title: "İletişim",
description: "Impact iletişim kanalları: e-posta, Instagram, LinkedIn, WhatsApp.",
};
const channels = [
{
key: "email",
title: "E-posta",
value: contact.email,
href: `mailto:${contact.email}`,
help: "Genel sorular, sponsor talepleri ve işbirlikleri.",
},
{
key: "instagram",
title: "Instagram",
value: `@${contact.instagram.handle}`,
href: contact.instagram.url,
help: "Etkinlik duyuruları, geçmiş fotoğraflar, anlık içerik.",
},
{
key: "linkedin",
title: "LinkedIn",
value: "impactcommunit",
href: contact.linkedin.url,
help: "Kurumsal iletişim, sponsor ve partner ağı.",
},
{
key: "whatsapp",
title: "WhatsApp",
value: "Topluluğa katıl",
href: contact.whatsapp.url,
help: "Katılımcı, gönüllü ve mentor topluluğu.",
},
];
const altMails = [
"contact@impact.tr",
"info@impact.tr",
"hello@impact.tr",
];
export default function ContactPage() {
return (
<>
<PageHero
eyebrow="İletişim"
title={<>Bize ulaşın</>}
lead="Sponsor, partner, gönüllü, mentor ya da sadece meraklı — hangi kanalı seçersen seç yanıt veriyoruz."
/>
{/* Channels */}
<Section eyebrow="Kanallar" title="Nereden ulaşabilirsiniz">
<div className="grid gap-4 md:grid-cols-2">
{channels.map((ch) => (
<a
key={ch.key}
href={ch.href}
target={ch.href.startsWith("http") ? "_blank" : undefined}
rel="noreferrer noopener"
className="group flex items-start justify-between gap-6 border-2 border-neutral-900 bg-white p-6 transition hover:-translate-y-1 hover:bg-yellow-50"
>
<div>
<div className="text-xs font-black uppercase tracking-[0.25em] text-violet-700">
{ch.title}
</div>
<div className="mt-2 text-2xl font-black tracking-tight">{ch.value}</div>
<div className="mt-1 text-sm text-neutral-600">{ch.help}</div>
</div>
<span
aria-hidden
className="mt-2 text-2xl transition group-hover:translate-x-1"
>
</span>
</a>
))}
</div>
</Section>
{/* Team emails */}
<Section tone="muted" eyebrow="Ekip" title="Doğrudan kurucu ekibe">
<div className="grid gap-4 md:grid-cols-3">
{team.map((m) => {
const handle = m.name.split(" ")[0].toLowerCase().replace(/ı/g, "i");
const email = `${handle}@impact.tr`;
return (
<a
key={m.name}
href={`mailto:${email}`}
className="group flex flex-col gap-5 border-2 border-neutral-900 bg-white p-6 transition hover:-translate-y-1 hover:bg-yellow-50"
>
<span className="inline-flex size-12 items-center justify-center bg-violet-700 text-sm font-black text-white">
{m.name
.split(" ")
.map((w) => w[0])
.slice(0, 2)
.join("")}
</span>
<div>
<div className="text-xl font-black tracking-tight">{m.name}</div>
<div className="mt-1 text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
{m.role}
</div>
</div>
<div className="border-t-2 border-neutral-900 pt-3 text-sm font-black text-violet-700 transition group-hover:translate-x-0.5">
{email}
</div>
</a>
);
})}
</div>
</Section>
{/* Alt mails */}
<Section eyebrow="Ek posta kutuları" title="Bunlara da yazabilirsiniz">
<div className="flex flex-wrap gap-2">
{altMails.map((m) => (
<a
key={m}
href={`mailto:${m}`}
className="border-2 border-neutral-900 bg-white px-4 py-2 text-sm font-black uppercase tracking-wider text-neutral-900 hover:bg-yellow-300"
>
{m}
</a>
))}
</div>
</Section>
</>
);
}

View File

@@ -1,6 +1,9 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Nav } from "@/components/Nav";
import { Footer } from "@/components/Footer";
import { site } from "@/lib/contact";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,21 +16,40 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
metadataBase: new URL(site.url),
title: {
default: "impact — STEM için, gençler tarafından",
template: "%s · impact",
},
description: site.description,
openGraph: {
type: "website",
url: site.url,
siteName: "impact",
title: "impact — STEM için, gençler tarafından",
description: site.description,
locale: "tr_TR",
},
twitter: {
card: "summary_large_image",
title: "impact",
description: site.description,
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
}: Readonly<{ children: React.ReactNode }>) {
return (
<html
lang="en"
lang="tr"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="flex min-h-full flex-col bg-white text-neutral-900">
<Nav />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}

View File

@@ -1,65 +1,400 @@
import Image from "next/image";
import Link from "next/link";
import { CompetitionCard } from "@/components/CompetitionCard";
import { EditionCard } from "@/components/EditionCard";
import { Gallery } from "@/components/Gallery";
import { Marquee } from "@/components/Marquee";
import { PhotoStrip } from "@/components/PhotoStrip";
import { Section } from "@/components/Section";
import { StatsStrip } from "@/components/StatsStrip";
import { TestimonialCard } from "@/components/TestimonialCard";
import { contact, values } from "@/lib/contact";
import {
formatDateRange,
getCompetitions,
getEditions,
getTotalStats,
getUpcomingEdition,
} from "@/lib/content";
import { photos, reviews } from "@/lib/reviews";
export default function Home() {
const competitions = getCompetitions();
const editions = getEditions();
const past = editions.filter((e) => e.status === "past");
const upcoming = getUpcomingEdition();
const totals = getTotalStats();
const review = reviews[0];
const heroPhoto = photos.cemberlitas[14];
const gallery = [3, 9, 15, 20, 24, 30].map((i) => photos.cemberlitas[i]);
const strip = [6, 17, 25, 36].map((i) => photos.cemberlitas[i]);
const latest = past[0];
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.
<>
{/* HERO */}
<section className="border-b-2 border-neutral-900">
<div className="grid md:grid-cols-12">
<div className="relative flex flex-col justify-between bg-violet-700 p-10 text-white md:col-span-7 md:p-16 lg:p-20">
{upcoming && (
<Link
href={`/yarismalar/${upcoming.competition}`}
className="inline-flex w-fit items-center gap-3 border border-white/30 bg-white/10 px-3 py-1 text-xs font-black uppercase tracking-[0.25em] hover:bg-white/20"
>
<span className="size-2 animate-pulse rounded-full bg-yellow-300" />
Sıradaki: {upcoming.name}
</Link>
)}
<div className="mt-12">
<h1 className="text-balance text-6xl font-black leading-[0.92] tracking-[-0.03em] md:text-7xl lg:text-[7rem]">
STEM.
<br />
<span className="text-yellow-300">Herkes.</span>
<br />
Hemen.
</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"
<p className="mt-8 max-w-lg text-lg leading-relaxed text-white/85">
Impact, Türkiye'de STEM ekosistemini genişleten bir gençlik topluluğu.
Lise ve üniversite öğrencileriyle hands-on robotik yarışmaları, ideathonlar
ve regional etkinlikler düzenliyoruz.
</p>
<div className="mt-10 flex flex-wrap gap-3">
<Link
href="/yarismalar/robo-soccer"
className="inline-flex items-center gap-2 bg-white px-6 py-3.5 text-sm font-black uppercase tracking-wider text-neutral-900 transition hover:bg-yellow-300"
>
Templates
</a>{" "}
or the{" "}
Robo Soccer →
</Link>
<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"
href={contact.whatsapp.url}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-2 border-2 border-white/30 px-6 py-3.5 text-sm font-black uppercase tracking-wider text-white transition hover:border-white"
>
Learning
</a>{" "}
center.
WhatsApp topluluk
</a>
</div>
</div>
<div className="mt-16 flex flex-wrap gap-8 border-t border-white/20 pt-8">
<HeroStat v={totals.editions.toString()} l="Edisyon" />
<HeroStat v={`${totals.teams}+`} l="Takım" />
<HeroStat v={`${totals.participants}+`} l="Katılımcı" />
<HeroStat v={`${totals.volunteers}+`} l="Gönüllü" />
</div>
</div>
<div className="relative md:col-span-5">
<div className="relative h-80 w-full md:h-full md:min-h-[560px]">
<Image
src={heroPhoto.src}
alt="Impact Hands-On Challenge Çemberlitaş Regional"
fill
priority
sizes="(min-width: 768px) 42vw, 100vw"
className="object-cover"
/>
</div>
</div>
</div>
</section>
{/* MARQUEE */}
<Marquee
items={[
"IMPACT.TR",
"HANDS-ON CHALLENGE",
"IDEATHON",
"ROBO SOCCER",
"STEM · TÜRKİYE",
]}
/>
{/* FORMATS */}
<Section
eyebrow="Ne yapıyoruz"
title={
<>
Üç format,{" "}
<br className="hidden md:inline" />
tek topluluk
</>
}
description="Fikirden prototipe, prototipten şampiyonaya — birbirini besleyen yarışma formatları."
>
<div className="space-y-4">
{competitions.map((c, i) => (
<CompetitionCard key={c.slug} c={c} index={i} />
))}
</div>
</Section>
{/* PHOTO STRIP */}
<section className="border-b-2 border-neutral-900 bg-white">
<div className="mx-auto max-w-[1400px] px-6 py-14 md:px-10">
<PhotoStrip photos={strip} />
</div>
</section>
{/* LATEST EDITION with rich photo treatment */}
{latest && (
<Section
tone="muted"
eyebrow="Son edisyon"
title={latest.name}
description={latest.summary}
>
<div className="grid gap-6 md:grid-cols-12">
<div className="md:col-span-7">
<div className="relative aspect-[4/3] border-2 border-neutral-900 bg-neutral-200">
<Image
src={photos.cemberlitas[20].src}
alt={`${latest.name} sahne fotoğrafı`}
fill
sizes="(min-width: 768px) 60vw, 100vw"
className="object-cover"
/>
</div>
<div className="mt-4 grid grid-cols-2 gap-4">
<div className="relative aspect-[4/3] border-2 border-neutral-900 bg-neutral-200">
<Image
src={photos.cemberlitas[2].src}
alt=""
fill
sizes="(min-width: 768px) 30vw, 50vw"
className="object-cover"
/>
</div>
<div className="relative aspect-[4/3] border-2 border-neutral-900 bg-neutral-200">
<Image
src={photos.cemberlitas[38].src}
alt=""
fill
sizes="(min-width: 768px) 30vw, 50vw"
className="object-cover"
/>
</div>
</div>
</div>
<div className="md:col-span-5">
{latest.stats && (
<StatsStrip
items={[
{ value: latest.stats.teams ?? 0, label: "Takım" },
{ value: `${latest.stats.participants ?? 0}+`, label: "Katılımcı" },
{ value: latest.stats.volunteers ?? 0, label: "Gönüllü" },
...(latest.startDate && latest.endDate
? [
{
value: String(
Math.round(
(new Date(latest.endDate).getTime() -
new Date(latest.startDate).getTime()) /
86400000,
) + 1,
),
label: "Gün",
},
]
: []),
]}
/>
)}
<div className="mt-6 border-2 border-neutral-900 bg-white p-6">
<dl className="grid grid-cols-2 gap-4 text-sm">
<div>
<dt className="text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500">
Tarih
</dt>
<dd className="mt-1 font-black">
{formatDateRange(latest.startDate, latest.endDate)}
</dd>
</div>
<div>
<dt className="text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500">
Mekan
</dt>
<dd className="mt-1 font-black">{latest.venue}</dd>
</div>
</dl>
<Link
href={`/edisyonlar/${latest.slug}`}
className="mt-6 inline-flex items-center gap-2 border-2 border-neutral-900 bg-white px-5 py-3 text-xs font-black uppercase tracking-wider hover:bg-yellow-300"
>
Edisyonu incele →
</Link>
</div>
</div>
</div>
</Section>
)}
{/* GALLERY */}
<Section eyebrow="Sahneden" title="Çemberlitaş Regional, 2 gün">
<Gallery photos={gallery} />
<div className="mt-8 flex justify-end">
<a
href={contact.instagram.url}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-2 bg-neutral-900 px-4 py-2 text-xs font-black uppercase tracking-wider text-white hover:bg-violet-700"
>
@impact.tr ↗
</a>
</div>
</Section>
{/* REVIEW */}
<Section tone="muted" eyebrow="Geri dönüşler" title="Katılımcı ve partner yorumları">
<div className="grid gap-10 md:grid-cols-12">
<div className="md:col-span-4">
<p className="text-neutral-700">
Deneyimini paylaşmak bir sonraki katılımcı için önemli. Etkinliklerimize
katıldıysan bize yaz; tüm yorumları burada yayınlıyoruz.
</p>
<a
href={`mailto:${contact.email}?subject=Impact%20deneyimim`}
className="mt-6 inline-flex items-center gap-2 border-2 border-neutral-900 bg-white px-5 py-3 text-xs font-black uppercase tracking-wider hover:bg-yellow-300"
>
Yorum gönder →
</a>
</div>
<div className="md:col-span-8">
<TestimonialCard t={review} featured />
</div>
</div>
</Section>
{/* EDITIONS */}
<Section eyebrow="Edisyonlar" title="Yaklaşan ve geçmiş etkinlikler">
<div className="grid gap-4 md:grid-cols-3">
{editions.slice(0, 6).map((e) => (
<EditionCard key={e.slug} e={e} />
))}
</div>
</Section>
{/* UPCOMING ROBO SOCCER */}
<Section
tone="yellow"
eyebrow="Yaklaşan sezon"
title={
<>
Robo Soccer
<br />
Sezon 01
</>
}
description="5 regional + championship. İstanbul ve Ankara ile başlıyor, üç şehir daha açıklanacak."
>
<div className="grid gap-8 md:grid-cols-12">
<div className="md:col-span-6">
<ol className="divide-y-2 divide-neutral-900 border-2 border-neutral-900 bg-white">
{[
{ n: "01", label: "İstanbul Regional", when: "Yakında" },
{ n: "02", label: "Ankara Regional", when: "Yakında" },
{ n: "03", label: "Regional #3", when: "Açıklanacak" },
{ n: "04", label: "Regional #4", when: "Açıklanacak" },
{ n: "05", label: "Regional #5", when: "Açıklanacak" },
{ n: "★", label: "Championship", when: "Sezon finali" },
].map((row) => (
<li key={row.n} className="flex items-center gap-5 px-5 py-4">
<span className="w-6 font-black text-violet-700">{row.n}</span>
<span className="flex-1 text-sm font-semibold">{row.label}</span>
<span className="text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
{row.when}
</span>
</li>
))}
</ol>
</div>
<div className="flex flex-col justify-between gap-6 md:col-span-6">
<div className="space-y-3 text-neutral-900">
<p className="text-lg">
Takımlar otonom robotlarıyla sahada karşılaşır. Regional turnuvalardan top
takımlar Championship'e kalır.
</p>
<p className="text-sm text-neutral-700">
Tarihler, başvuru formu ve kit spesifikasyonları için topluluk kanallarımızı
takip et.
</p>
</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"
<div className="flex flex-wrap gap-3">
<Link
href="/yarismalar/robo-soccer"
className="bg-neutral-900 px-6 py-3.5 text-xs font-black uppercase tracking-wider text-white hover:bg-violet-700"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
Detaylar
</Link>
<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"
href={contact.whatsapp.url}
target="_blank"
rel="noopener noreferrer"
rel="noreferrer noopener"
className="border-2 border-neutral-900 bg-white px-6 py-3.5 text-xs font-black uppercase tracking-wider hover:bg-white/70"
>
Documentation
WhatsApp topluluk
</a>
</div>
</main>
</div>
</div>
</Section>
{/* VALUES + CTA */}
<Section tone="dark">
<div className="grid gap-12 md:grid-cols-12">
<div className="md:col-span-7">
<div className="text-xs font-black uppercase tracking-[0.25em] text-yellow-300">
Sponsor olun
</div>
<h3 className="mt-4 text-balance text-4xl font-black leading-[1] tracking-tight md:text-5xl">
Geleceğin mühendislerine alan ın.
</h3>
<p className="mt-5 max-w-xl text-neutral-300">
Impact etkinliklerinde sponsorlar, yüzlerce katılımcının üretim maratonlarında
görünür olur; kampüs erişimi, yetenek havuzu ve STEM topluluğu inşasına
katkı sağlar.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/sponsor"
className="bg-yellow-300 px-6 py-4 text-xs font-black uppercase tracking-wider text-neutral-900 hover:bg-white"
>
Sponsor paketleri
</Link>
<a
href={`mailto:${contact.email}?subject=Impact%20sponsorluk`}
className="border-2 border-white/30 px-6 py-4 text-xs font-black uppercase tracking-wider text-white hover:border-white"
>
{contact.email}
</a>
</div>
</div>
<div className="md:col-span-5">
<div className="text-xs font-black uppercase tracking-[0.25em] text-yellow-300">
Değerlerimiz
</div>
<div className="mt-4 flex flex-wrap gap-2">
{values.map((v) => (
<span
key={v}
className="border-2 border-white/30 bg-white/5 px-4 py-2 text-xs font-black uppercase tracking-[0.2em]"
>
{v}
</span>
))}
</div>
</div>
</div>
</Section>
</>
);
}
function HeroStat({ v, l }: { v: string; l: string }) {
return (
<div>
<div className="text-3xl font-black md:text-4xl">{v}</div>
<div className="mt-1 text-[10px] font-black uppercase tracking-[0.25em] text-white/70">
{l}
</div>
</div>
);
}

235
src/app/sponsor/page.tsx Normal file
View File

@@ -0,0 +1,235 @@
import type { Metadata } from "next";
import Link from "next/link";
import { PageHero } from "@/components/PageHero";
import { PhotoStrip } from "@/components/PhotoStrip";
import { Section } from "@/components/Section";
import { contact } from "@/lib/contact";
import { getTotalStats } from "@/lib/content";
import { photos } from "@/lib/reviews";
export const metadata: Metadata = {
title: "Sponsor Ol",
description:
"Impact sponsorluk paketleri. STEM topluluğuyla kampüs erişimi, marka görünürlüğü ve sosyal etki.",
};
type Tier = {
name: string;
slogan: string;
perks: string[];
highlight?: boolean;
};
const tiers: Tier[] = [
{
name: "Platin",
slogan: "Ana sponsor · tüm regional + championship",
highlight: true,
perks: [
"Etkinlik isim hakkı (naming rights) seçeneği",
"Tüm regional + championship ana sponsoru",
"Sahne önü logo, arena branding, roll-up ve bayraklar",
"Katılımcı kit'inde logo",
"Opt-in katılımcı veritabanına erişim (KVKK uyumlu)",
"Kampüs CV toplama + kariyer günü slotu",
"Basın bülteninde ana ortak",
],
},
{
name: "Altın",
slogan: "2 regional ana sponsoru",
perks: [
"Seçilen 2 regional'da premium logo konumu",
"Arena branding + sosyal medya kampanya serisi",
"Mentor / konuşmacı slotu",
"Katılımcı kit'inde logo",
"Kısa opt-in katılımcı listesi",
],
},
{
name: "Gümüş",
slogan: "1 regional destekçisi",
perks: [
"Seçilen regional'da logo görünürlüğü",
"Sosyal medya + etkinlik duyurularında yer",
"Sahne sunumunda teşekkür",
"Mentor slotu (opsiyonel)",
],
},
{
name: "Bronz",
slogan: "Destekçi",
perks: [
"Logo tüm dijital materyallerde",
"Teşekkür postu",
"İsim sponsor listesinde",
],
},
];
const inKind = [
"Kit / malzeme (ESP32, sensör, motor)",
"Yemek & içecek",
"Mekan / salon",
"Baskı, roll-up, tişört",
"Ödül (laptop, kit, stajyerlik fırsatı)",
];
export default function SponsorPage() {
const totals = getTotalStats();
const heroPhoto = photos.cemberlitas[25];
const strip = [1, 13, 29, 35].map((i) => photos.cemberlitas[i]);
return (
<>
<PageHero
eyebrow="Sponsor olun"
title={<>Sahada olun, bir nesle dokunun</>}
lead={`Impact etkinliklerinde sponsorlar, ${totals.participants}+ katılımcı ve ${totals.teams}+ takımın üretim maratonlarında görünür olur; kampüs erişimi, yetenek havuzu ve STEM topluluğu inşasına katkı sağlar.`}
variant="photo"
photo={{ src: heroPhoto.src, alt: "Impact etkinlik salonu" }}
/>
{/* Why */}
<Section eyebrow="Neden Impact" title="Sponsorlar için ne sunuyoruz">
<div className="grid gap-4 md:grid-cols-3">
<Perk
title="Yetenek havuzu"
body="Türkiye'nin en motive lise ve üniversite öğrencileri. Opt-in veritabanı (KVKK uyumlu), staj ve işe alım kanalı."
/>
<Perk
title="Marka görünürlüğü"
body="Arena branding, katılımcı kit'i, etkinlik öncesi/sonrası dijital kampanya, basın bülteni ve sosyal medya serisi."
/>
<Perk
title="Sosyal etki raporu"
body="Her edisyon sonrası ölçülmüş metrikler: katılımcı, gönüllü, şehir dağılımı, sosyoekonomik erişim — CSR için hazır veri."
/>
</div>
</Section>
{/* PHOTO STRIP */}
<section className="border-b-2 border-neutral-900 bg-neutral-50">
<div className="mx-auto max-w-[1400px] px-6 py-14 md:px-10">
<PhotoStrip photos={strip} />
</div>
</section>
{/* Tiers */}
<Section eyebrow="Paketler" title="Sponsorluk paketleri">
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-4">
{tiers.map((t) => (
<div
key={t.name}
className={`relative flex flex-col border-2 border-neutral-900 p-6 ${
t.highlight ? "bg-violet-700 text-white" : "bg-white text-neutral-900"
}`}
>
{t.highlight && (
<div className="absolute -left-3 -top-3 bg-yellow-300 px-3 py-1 text-[10px] font-black uppercase tracking-[0.2em] text-neutral-900">
Önerilen
</div>
)}
<div className="text-3xl font-black tracking-tight">{t.name}</div>
<div
className={`mt-1 text-sm font-semibold ${
t.highlight ? "text-white/85" : "text-neutral-600"
}`}
>
{t.slogan}
</div>
<ul className="mt-6 flex-1 space-y-2 text-sm">
{t.perks.map((p) => (
<li key={p} className="flex gap-2">
<span
className={`mt-1.5 inline-block size-1.5 shrink-0 ${
t.highlight ? "bg-yellow-300" : "bg-violet-700"
}`}
/>
<span className={t.highlight ? "text-white/85" : "text-neutral-700"}>
{p}
</span>
</li>
))}
</ul>
<a
href={`mailto:${contact.email}?subject=${encodeURIComponent(
`Impact Sponsorluk — ${t.name}`,
)}`}
className={`mt-6 inline-flex w-fit items-center gap-2 px-4 py-2.5 text-xs font-black uppercase tracking-wider transition ${
t.highlight
? "bg-yellow-300 text-neutral-900 hover:bg-white"
: "border-2 border-neutral-900 bg-white hover:bg-yellow-300"
}`}
>
{t.name} için yaz
</a>
</div>
))}
</div>
<p className="mt-6 text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
Paket fiyatlamaları etkinlik ölçeği ve kapsama göre özelleştirilir. Detaylı deck için bize yazın.
</p>
</Section>
{/* In-kind */}
<Section tone="muted" eyebrow="Ayni destek" title="Parasal olmayan katkılar">
<ul className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{inKind.map((k) => (
<li
key={k}
className="border-2 border-neutral-900 bg-white px-5 py-4 text-sm font-semibold"
>
{k}
</li>
))}
</ul>
</Section>
{/* CTA */}
<Section tone="yellow">
<div className="flex flex-col items-start gap-6 md:flex-row md:items-end md:justify-between">
<div>
<div className="text-xs font-black uppercase tracking-[0.25em] text-violet-700">
Konuşalım
</div>
<h3 className="mt-3 text-balance text-4xl font-black leading-[1] tracking-tight md:text-5xl">
Deck için iki satır atmanız yeterli
</h3>
</div>
<div className="flex flex-wrap gap-3">
<a
href={`mailto:${contact.email}?subject=Impact%20Sponsorluk`}
className="bg-neutral-900 px-6 py-4 text-xs font-black uppercase tracking-wider text-white hover:bg-violet-700"
>
{contact.email}
</a>
<a
href={contact.linkedin.url}
target="_blank"
rel="noreferrer noopener"
className="border-2 border-neutral-900 bg-white px-6 py-4 text-xs font-black uppercase tracking-wider hover:bg-neutral-900 hover:text-white"
>
LinkedIn
</a>
<Link
href="/iletisim"
className="px-6 py-4 text-xs font-black uppercase tracking-wider text-neutral-800 hover:text-violet-700"
>
Tüm kanallar
</Link>
</div>
</div>
</Section>
</>
);
}
function Perk({ title, body }: { title: string; body: string }) {
return (
<div className="border-2 border-neutral-900 bg-white p-6">
<div className="text-xl font-black tracking-tight">{title}</div>
<p className="mt-3 text-sm text-neutral-700">{body}</p>
</div>
);
}

View File

@@ -0,0 +1,160 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { EditionCard } from "@/components/EditionCard";
import { PageHero } from "@/components/PageHero";
import { PhotoStrip } from "@/components/PhotoStrip";
import { Section } from "@/components/Section";
import {
getCompetition,
getCompetitions,
getEditionsFor,
type CompetitionSlug,
} from "@/lib/content";
import { contact } from "@/lib/contact";
import { photos } from "@/lib/reviews";
export function generateStaticParams() {
return getCompetitions().map((c) => ({ slug: c.slug }));
}
export async function generateMetadata(
props: PageProps<"/yarismalar/[slug]">,
): Promise<Metadata> {
const { slug } = await props.params;
const c = getCompetition(slug);
if (!c) return {};
return {
title: c.name,
description: c.tagline,
};
}
export default async function CompetitionPage(
props: PageProps<"/yarismalar/[slug]">,
) {
const { slug } = await props.params;
const c = getCompetition(slug);
if (!c) notFound();
const editions = getEditionsFor(slug as CompetitionSlug);
const upcoming = editions.filter((e) => e.status !== "past");
const past = editions.filter((e) => e.status === "past");
const heroPhoto =
c.slug === "hands-on-challenge" ? photos.cemberlitas[7] : undefined;
const strip =
c.slug === "hands-on-challenge"
? [0, 19, 26, 37].map((i) => photos.cemberlitas[i])
: [];
const tags = (
<>
{c.categories?.map((cat) => (
<span
key={cat}
className="border border-white/50 bg-white/10 px-3 py-1 text-xs font-black uppercase tracking-[0.2em]"
>
{cat}
</span>
))}
{c.ageGroups?.map((a) => (
<span
key={a}
className="bg-yellow-300 px-3 py-1 text-xs font-black uppercase tracking-[0.2em] text-neutral-900"
>
{a}
</span>
))}
</>
);
return (
<>
<PageHero
eyebrow="Yarışma formatı"
title={c.name}
lead={c.tagline}
tags={tags}
variant={heroPhoto ? "photo" : "solid"}
photo={heroPhoto ? { src: heroPhoto.src, alt: `${c.name} sahne` } : undefined}
/>
{/* Upcoming */}
{upcoming.length > 0 && (
<Section eyebrow="Yaklaşan" title="Sıradaki edisyonlar">
<div className="grid gap-4 md:grid-cols-3">
{upcoming.map((e) => (
<EditionCard key={e.slug} e={e} />
))}
</div>
</Section>
)}
{/* Format body */}
<Section tone="muted" eyebrow="Format" title="Nasıl işliyor">
<div
className="prose-impact max-w-3xl"
dangerouslySetInnerHTML={{ __html: c.bodyHtml }}
/>
</Section>
{/* Photo strip (for Hands-On) */}
{strip.length > 0 && (
<section className="border-b-2 border-neutral-900 bg-white">
<div className="mx-auto max-w-[1400px] px-6 py-14 md:px-10">
<PhotoStrip photos={strip} />
</div>
</section>
)}
{/* Past editions */}
{past.length > 0 && (
<Section eyebrow="Geçmiş" title="Önceki edisyonlar">
<div className="grid gap-4 md:grid-cols-3">
{past.map((e) => (
<EditionCard key={e.slug} e={e} />
))}
</div>
</Section>
)}
{/* CTA */}
<Section tone="yellow">
<div className="flex flex-col items-start gap-6 md:flex-row md:items-center md:justify-between">
<div>
<h3 className="text-balance text-3xl font-black leading-tight tracking-tight md:text-4xl">
Başvuru veya sorularınız için
</h3>
<p className="mt-3 max-w-xl text-neutral-800">
Bir sonraki edisyon için haberdar olmak ister misin? Bize yaz ya da WhatsApp
grubuna katıl.
</p>
</div>
<div className="flex flex-wrap gap-3">
<a
href={`mailto:${contact.email}?subject=${encodeURIComponent(c.name)}`}
className="bg-neutral-900 px-5 py-3.5 text-xs font-black uppercase tracking-wider text-white hover:bg-violet-700"
>
{contact.email}
</a>
<a
href={contact.whatsapp.url}
target="_blank"
rel="noreferrer noopener"
className="border-2 border-neutral-900 bg-white px-5 py-3.5 text-xs font-black uppercase tracking-wider hover:bg-neutral-900 hover:text-white"
>
WhatsApp topluluk
</a>
<Link
href="/yarismalar/hands-on-challenge"
className="px-5 py-3.5 text-xs font-black uppercase tracking-wider text-neutral-800 hover:text-violet-700"
>
Diğer formatlar
</Link>
</div>
</div>
</Section>
</>
);
}

View File

@@ -0,0 +1,43 @@
import Link from "next/link";
import type { Competition } from "@/lib/content";
export function CompetitionCard({ c, index }: { c: Competition; index?: number }) {
return (
<Link
href={`/yarismalar/${c.slug}`}
className="group flex items-start gap-6 border-2 border-neutral-900 bg-white p-6 transition hover:-translate-y-1 hover:bg-yellow-50 md:p-8"
>
<div className="text-4xl font-black text-violet-700 md:text-5xl">
{typeof index === "number"
? String(index + 1).padStart(2, "0")
: c.slug === "hands-on-challenge"
? "01"
: c.slug === "ideathon"
? "02"
: "03"}
</div>
<div className="flex-1">
<div className="flex flex-wrap items-baseline gap-3">
<h3 className="text-2xl font-black tracking-tight md:text-3xl">{c.name}</h3>
<span className="text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
{c.durationDays ? `${c.durationDays} gün` : c.short}
</span>
</div>
<p className="mt-3 text-neutral-700">{c.tagline}</p>
<div className="mt-4 flex flex-wrap gap-2">
{c.categories?.slice(0, 3).map((cat) => (
<span
key={cat}
className="border border-neutral-900 bg-white px-2 py-0.5 text-[11px] font-bold uppercase tracking-wider text-neutral-700"
>
{cat}
</span>
))}
</div>
</div>
<span className="mt-2 text-2xl text-neutral-900 transition group-hover:translate-x-1">
</span>
</Link>
);
}

View File

@@ -0,0 +1,62 @@
import Link from "next/link";
import type { Edition } from "@/lib/content";
import { formatDateRange } from "@/lib/content";
const statusLabel = {
upcoming: "Yaklaşan",
announced: "Duyuruldu",
past: "Geçmiş",
} as const;
export function EditionCard({ e }: { e: Edition }) {
const isPast = e.status === "past";
return (
<Link
href={`/edisyonlar/${e.slug}`}
className="group flex flex-col border-2 border-neutral-900 bg-white p-5 transition hover:-translate-y-1 hover:bg-yellow-50"
>
<div className="flex items-center justify-between">
<span
className={`border border-neutral-900 px-2 py-0.5 text-[10px] font-black uppercase tracking-[0.2em] ${
isPast ? "bg-white text-neutral-900" : "bg-yellow-300 text-neutral-900"
}`}
>
{statusLabel[e.status]}
</span>
{e.city && (
<span className="text-xs font-black uppercase tracking-[0.2em] text-neutral-500">
{e.city}
</span>
)}
</div>
<div className="mt-4 text-xl font-black leading-tight tracking-tight">{e.name}</div>
<div className="mt-1 text-sm font-semibold text-neutral-600">
{formatDateRange(e.startDate, e.endDate)}
</div>
{e.venue && (
<div className="text-xs text-neutral-500">{e.venue}</div>
)}
{e.stats && (
<div className="mt-5 grid grid-cols-3 gap-3 border-t-2 border-neutral-900 pt-4 text-xs">
{e.stats.teams != null && <Stat v={e.stats.teams} l="Takım" />}
{e.stats.participants != null && <Stat v={`${e.stats.participants}+`} l="Katılımcı" />}
{e.stats.volunteers != null && <Stat v={e.stats.volunteers} l="Gönüllü" />}
</div>
)}
<span className="mt-4 inline-flex items-center gap-1 text-xs font-black uppercase tracking-[0.2em] text-violet-700 transition group-hover:translate-x-0.5">
</span>
</Link>
);
}
function Stat({ v, l }: { v: string | number; l: string }) {
return (
<div>
<div className="text-lg font-black text-neutral-900">{v}</div>
<div className="text-[10px] font-black uppercase tracking-[0.15em] text-neutral-500">
{l}
</div>
</div>
);
}

106
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,106 @@
import Link from "next/link";
import { contact, site } from "@/lib/contact";
export function Footer() {
const year = new Date().getFullYear();
return (
<footer className="border-t-2 border-neutral-900 bg-white">
<div className="mx-auto max-w-[1400px] px-6 py-16 md:px-10">
<div className="grid gap-10 md:grid-cols-12">
<div className="md:col-span-5">
<div className="inline-flex items-center gap-2">
<span className="inline-flex size-10 items-center justify-center rounded-sm bg-violet-700 text-sm font-black text-white">
i
</span>
<span className="text-2xl font-black lowercase tracking-tight">impact</span>
</div>
<p className="mt-5 max-w-sm text-sm text-neutral-600">{site.description}</p>
</div>
<div className="md:col-span-3">
<h4 className="text-xs font-black uppercase tracking-[0.2em] text-violet-700">
Yarışmalar
</h4>
<ul className="mt-4 space-y-2 text-sm font-semibold">
<FooterLink href="/yarismalar/hands-on-challenge">Hands-On Challenge</FooterLink>
<FooterLink href="/yarismalar/ideathon">Ideathon</FooterLink>
<FooterLink href="/yarismalar/robo-soccer">Robo Soccer</FooterLink>
</ul>
</div>
<div className="md:col-span-2">
<h4 className="text-xs font-black uppercase tracking-[0.2em] text-violet-700">
Kurum
</h4>
<ul className="mt-4 space-y-2 text-sm font-semibold">
<FooterLink href="/hakkimizda">Hakkımızda</FooterLink>
<FooterLink href="/sponsor">Sponsor Ol</FooterLink>
<FooterLink href="/iletisim">İletişim</FooterLink>
</ul>
</div>
<div className="md:col-span-2">
<h4 className="text-xs font-black uppercase tracking-[0.2em] text-violet-700">
Takip
</h4>
<ul className="mt-4 space-y-2 text-sm font-semibold">
<li>
<a
href={contact.instagram.url}
target="_blank"
rel="noreferrer noopener"
className="text-neutral-800 hover:text-violet-700"
>
Instagram
</a>
</li>
<li>
<a
href={contact.linkedin.url}
target="_blank"
rel="noreferrer noopener"
className="text-neutral-800 hover:text-violet-700"
>
LinkedIn
</a>
</li>
<li>
<a
href={contact.whatsapp.url}
target="_blank"
rel="noreferrer noopener"
className="text-neutral-800 hover:text-violet-700"
>
WhatsApp
</a>
</li>
<li>
<a
href={`mailto:${contact.email}`}
className="text-neutral-800 hover:text-violet-700"
>
{contact.email}
</a>
</li>
</ul>
</div>
</div>
<div className="mt-14 flex flex-col items-start justify-between gap-3 border-t-2 border-neutral-900 pt-6 text-xs font-black uppercase tracking-[0.15em] md:flex-row md:items-center">
<span>© {year} impact · {site.domain}</span>
<span className="text-neutral-500">STEM için, gençler tarafından.</span>
</div>
</div>
</footer>
);
}
function FooterLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<li>
<Link href={href} className="text-neutral-800 hover:text-violet-700">
{children}
</Link>
</li>
);
}

View File

@@ -0,0 +1,36 @@
import Image from "next/image";
export type GalleryPhoto = { src: string; alt?: string };
const spans = [
"col-span-12 md:col-span-8 aspect-[16/9]",
"col-span-6 md:col-span-4 aspect-square",
"col-span-6 md:col-span-4 aspect-square",
"col-span-12 md:col-span-4 aspect-[4/3]",
"col-span-6 md:col-span-4 aspect-[4/3]",
"col-span-6 md:col-span-4 aspect-[4/3]",
];
export function Gallery({ photos }: { photos: GalleryPhoto[] }) {
const take = photos.slice(0, 6);
return (
<div className="grid grid-cols-12 gap-3 md:gap-4">
{take.map((p, i) => (
<div
key={p.src}
className={`relative overflow-hidden border-2 border-neutral-900 bg-neutral-200 ${
spans[i] ?? "col-span-6 aspect-square"
}`}
>
<Image
src={p.src}
alt={p.alt ?? "Impact etkinlik"}
fill
sizes="(min-width: 768px) 33vw, 100vw"
className="object-cover transition duration-700 hover:scale-[1.03]"
/>
</div>
))}
</div>
);
}

19
src/components/Logo.tsx Normal file
View File

@@ -0,0 +1,19 @@
import Link from "next/link";
export function Logo({ className = "" }: { className?: string }) {
return (
<Link
href="/"
className={`group inline-flex items-center gap-2 text-neutral-900 ${className}`}
aria-label="impact anasayfa"
>
<span
aria-hidden
className="inline-flex size-8 items-center justify-center rounded-sm bg-violet-700 text-sm font-black leading-none text-white"
>
i
</span>
<span className="text-lg font-black lowercase tracking-tight">impact</span>
</Link>
);
}

View File

@@ -0,0 +1,32 @@
export function Marquee({
items,
tone = "paper",
}: {
items: string[];
tone?: "paper" | "dark" | "violet" | "yellow";
}) {
const bg =
tone === "dark"
? "bg-neutral-900 text-white"
: tone === "violet"
? "bg-violet-700 text-white"
: tone === "yellow"
? "bg-yellow-300 text-neutral-900"
: "bg-white text-neutral-900";
const dot = tone === "paper" ? "text-violet-700" : "text-yellow-300";
const row = items.flatMap((it, i) => [
<span key={`${i}-t`} className="px-6">{it}</span>,
<span key={`${i}-d`} className={dot}></span>,
]);
return (
<div
className={`overflow-hidden border-b-2 border-neutral-900 ${bg}`}
aria-hidden
>
<div className="marquee-track flex items-center whitespace-nowrap py-4 text-2xl font-black uppercase tracking-tight md:text-3xl">
<div className="flex items-center">{row}</div>
<div className="flex items-center">{row}</div>
</div>
</div>
);
}

100
src/components/Nav.tsx Normal file
View File

@@ -0,0 +1,100 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { Logo } from "./Logo";
import { contact } from "@/lib/contact";
const links = [
{ href: "/yarismalar/hands-on-challenge", label: "Hands-On" },
{ href: "/yarismalar/ideathon", label: "Ideathon" },
{ href: "/yarismalar/robo-soccer", label: "Robo Soccer" },
{ href: "/hakkimizda", label: "Hakkımızda" },
{ href: "/iletisim", label: "İletişim" },
];
export function Nav() {
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-40 border-b-2 border-neutral-900 bg-white">
<div className="mx-auto flex max-w-[1400px] items-center justify-between px-6 py-3 md:px-10">
<Logo />
<nav className="hidden items-center gap-7 text-sm font-black uppercase tracking-wider md:flex">
{links.map((l) => (
<Link
key={l.href}
href={l.href}
className="text-neutral-700 transition hover:text-violet-700"
>
{l.label}
</Link>
))}
</nav>
<div className="flex items-center gap-2">
<Link
href="/sponsor"
className="hidden items-center gap-1 bg-violet-700 px-4 py-2.5 text-xs font-black uppercase tracking-wider text-white transition hover:bg-violet-800 md:inline-flex"
>
Sponsor Ol
</Link>
<a
href={contact.whatsapp.url}
target="_blank"
rel="noreferrer noopener"
className="hidden items-center gap-1 border-2 border-neutral-900 px-4 py-2 text-xs font-black uppercase tracking-wider text-neutral-900 transition hover:bg-yellow-300 md:inline-flex"
>
Topluluk
</a>
<button
type="button"
aria-label="Menü"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
className="inline-flex size-10 items-center justify-center border-2 border-neutral-900 md:hidden"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
{open ? (
<path
d="M6 6l12 12M18 6L6 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
) : (
<path
d="M4 7h16M4 12h16M4 17h16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
)}
</svg>
</button>
</div>
</div>
{open && (
<div className="border-t-2 border-neutral-900 md:hidden">
<div className="mx-auto flex max-w-[1400px] flex-col divide-y-2 divide-neutral-900 px-6 md:px-10">
{links.map((l) => (
<Link
key={l.href}
href={l.href}
onClick={() => setOpen(false)}
className="py-4 text-sm font-black uppercase tracking-wider text-neutral-900 hover:text-violet-700"
>
{l.label}
</Link>
))}
<Link
href="/sponsor"
onClick={() => setOpen(false)}
className="py-4 text-sm font-black uppercase tracking-wider text-violet-700"
>
Sponsor Ol
</Link>
</div>
</div>
)}
</header>
);
}

View File

@@ -0,0 +1,73 @@
import Image from "next/image";
import type { ReactNode } from "react";
export function PageHero({
eyebrow,
title,
lead,
tags,
photo,
variant = "solid",
children,
}: {
eyebrow: string;
title: ReactNode;
lead?: string;
tags?: ReactNode;
photo?: { src: string; alt?: string };
variant?: "solid" | "photo";
children?: ReactNode;
}) {
if (variant === "photo" && photo) {
return (
<section className="relative border-b-2 border-neutral-900">
<div className="relative">
<div className="relative h-[520px] w-full md:h-[620px]">
<Image
src={photo.src}
alt={photo.alt ?? ""}
fill
priority
sizes="100vw"
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-br from-violet-900/85 via-violet-800/60 to-neutral-900/75" />
</div>
<div className="absolute inset-0 flex items-end">
<div className="mx-auto w-full max-w-[1400px] px-6 pb-14 md:px-10 md:pb-20">
<div className="text-xs font-black uppercase tracking-[0.25em] text-yellow-300">
{eyebrow}
</div>
<h1 className="mt-4 text-balance text-5xl font-black leading-[0.95] tracking-[-0.03em] text-white md:text-7xl lg:text-[6rem]">
{title}
</h1>
{lead && (
<p className="mt-5 max-w-2xl text-lg text-white/85 md:text-xl">{lead}</p>
)}
{tags && <div className="mt-8 flex flex-wrap gap-2">{tags}</div>}
{children}
</div>
</div>
</div>
</section>
);
}
return (
<section className="border-b-2 border-neutral-900 bg-violet-700 text-white">
<div className="mx-auto max-w-[1400px] px-6 py-20 md:px-10 md:py-28">
<div className="text-xs font-black uppercase tracking-[0.25em] text-yellow-300">
{eyebrow}
</div>
<h1 className="mt-4 text-balance text-5xl font-black leading-[0.92] tracking-[-0.03em] md:text-7xl lg:text-[6rem]">
{title}
</h1>
{lead && (
<p className="mt-6 max-w-2xl text-lg text-white/85 md:text-xl">{lead}</p>
)}
{tags && <div className="mt-8 flex flex-wrap gap-2">{tags}</div>}
{children}
</div>
</section>
);
}

View File

@@ -0,0 +1,22 @@
import Image from "next/image";
export function PhotoStrip({ photos }: { photos: { src: string; alt?: string }[] }) {
return (
<div className="grid grid-cols-2 gap-3 md:grid-cols-4 md:gap-4">
{photos.slice(0, 4).map((p) => (
<div
key={p.src}
className="relative aspect-[4/5] overflow-hidden border-2 border-neutral-900 bg-neutral-200"
>
<Image
src={p.src}
alt={p.alt ?? "Impact etkinlik"}
fill
sizes="(min-width: 768px) 22vw, 50vw"
className="object-cover"
/>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,66 @@
import type { ReactNode } from "react";
export function Section({
id,
eyebrow,
title,
description,
children,
className = "",
tone = "paper",
}: {
id?: string;
eyebrow?: string;
title?: ReactNode;
description?: string;
children: ReactNode;
className?: string;
tone?: "paper" | "muted" | "violet" | "dark" | "yellow";
}) {
const bg =
tone === "muted"
? "bg-neutral-50 text-neutral-900"
: tone === "violet"
? "bg-violet-700 text-white"
: tone === "dark"
? "bg-neutral-900 text-white"
: tone === "yellow"
? "bg-yellow-300 text-neutral-900"
: "bg-white text-neutral-900";
const eyebrowColor =
tone === "violet" || tone === "dark"
? "text-yellow-300"
: tone === "yellow"
? "text-violet-700"
: "text-violet-700";
return (
<section
id={id}
className={`border-b-2 border-neutral-900 ${bg} ${className}`}
>
<div className="mx-auto max-w-[1400px] px-6 py-20 md:px-10 md:py-24">
{(eyebrow || title || description) && (
<div className="mb-12 max-w-3xl md:mb-16">
{eyebrow && (
<div
className={`text-xs font-black uppercase tracking-[0.25em] ${eyebrowColor}`}
>
{eyebrow}
</div>
)}
{title && (
<h2 className="mt-4 text-balance text-4xl font-black leading-[0.95] tracking-tight md:text-6xl">
{title}
</h2>
)}
{description && (
<p className="mt-5 text-lg text-current/75 md:text-xl">{description}</p>
)}
</div>
)}
{children}
</div>
</section>
);
}

View File

@@ -0,0 +1,26 @@
export function StatsStrip({
items,
tone = "paper",
}: {
items: { value: string | number; label: string }[];
tone?: "paper" | "dark" | "violet";
}) {
const bg =
tone === "dark"
? "bg-neutral-900 text-white"
: tone === "violet"
? "bg-violet-700 text-white"
: "bg-white text-neutral-900";
return (
<dl className={`grid grid-cols-2 divide-x-2 divide-y-2 divide-neutral-900 border-2 border-neutral-900 md:grid-cols-4 md:divide-y-0 ${bg}`}>
{items.map((it) => (
<div key={it.label} className="px-6 py-8 text-center">
<dd className="text-4xl font-black tracking-tight md:text-5xl">{it.value}</dd>
<dt className="mt-2 text-xs font-black uppercase tracking-[0.2em] opacity-70">
{it.label}
</dt>
</div>
))}
</dl>
);
}

View File

@@ -0,0 +1,67 @@
import type { Review } from "@/lib/reviews";
export function TestimonialCard({
t,
featured = false,
}: {
t: Review | { quote: string; by: string; org?: string };
featured?: boolean;
}) {
return (
<figure
className={`relative border-2 border-neutral-900 ${
featured ? "bg-violet-700 text-white" : "bg-white text-neutral-900"
} p-8 md:p-10`}
>
{featured && (
<div className="absolute -left-3 -top-3 bg-yellow-300 px-3 py-1 text-[10px] font-black uppercase tracking-[0.2em] text-neutral-900">
Featured
</div>
)}
<svg
aria-hidden
className={`size-10 ${featured ? "text-yellow-300" : "text-violet-700"}`}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M7.17 6C4.87 6 3 7.87 3 10.17v7.5H10.5v-7.5H6.33c0-1.01.82-1.83 1.84-1.83h1.5V6H7.17Zm9 0C13.87 6 12 7.87 12 10.17v7.5h7.5v-7.5h-4.17c0-1.01.82-1.83 1.84-1.83h1.5V6h-2.5Z" />
</svg>
<blockquote
className={`mt-5 text-pretty font-semibold leading-snug ${
featured ? "text-xl md:text-2xl" : "text-lg md:text-xl"
}`}
>
&ldquo;{t.quote}&rdquo;
</blockquote>
<figcaption
className={`mt-8 flex items-center gap-3 border-t-2 ${
featured ? "border-white/20" : "border-neutral-900"
} pt-5`}
>
<span
className={`inline-flex size-10 items-center justify-center text-sm font-black ${
featured ? "bg-yellow-300 text-neutral-900" : "bg-neutral-900 text-white"
}`}
>
{t.by
.split(" ")
.map((w) => w[0])
.slice(0, 2)
.join("")}
</span>
<span>
<span className="block text-sm font-black uppercase tracking-wider">{t.by}</span>
{t.org && (
<span
className={`block text-xs ${
featured ? "text-white/75" : "text-neutral-500"
}`}
>
{t.org}
</span>
)}
</span>
</figcaption>
</figure>
);
}

52
src/lib/contact.ts Normal file
View File

@@ -0,0 +1,52 @@
export const contact = {
email: "contact@impact.tr",
emails: {
contact: "contact@impact.tr",
info: "info@impact.tr",
hello: "hello@impact.tr",
tarik: "tarik@impact.tr",
tuna: "tuna@impact.tr",
yigit: "yigit@impact.tr",
},
instagram: {
handle: "impact.tr",
url: "https://www.instagram.com/impact.tr/",
},
linkedin: {
url: "https://www.linkedin.com/company/impactcommunit/",
},
whatsapp: {
url: "https://chat.whatsapp.com/L26ZtCldCFdDJJMaorM0dm",
},
} as const;
export const site = {
name: "impact",
domain: "impact.tr",
url: "https://impact.tr",
tagline: "Bilim, teknoloji, mühendislik ve matematiği herkes için erişilebilir kılıyoruz.",
description:
"Impact, Türkiye'de STEM ekosistemini genişleten bir gençlik topluluğu. Hands-on robotik yarışmaları, ideathonlar ve eğitim programları düzenliyoruz.",
} as const;
export const team = [
{ name: "Tarık Seyhan", role: "Kurucu · Yönetim" },
{ name: "Tuna Gül", role: "Kurucu · Yönetim" },
{ name: "Yiğit Efe Erdoğmuş", role: "Kurucu · Yönetim" },
] as const;
export const values = [
"Education",
"Innovation",
"Social Responsibility",
"Future",
"Sustainability",
"Ethics",
] as const;
export const sdgs = [
{ number: 4, name: "Quality Education" },
{ number: 5, name: "Gender Equality" },
{ number: 9, name: "Industry, Innovation and Infrastructure" },
{ number: 10, name: "Reduced Inequalities" },
] as const;

172
src/lib/content.ts Normal file
View File

@@ -0,0 +1,172 @@
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
import { marked } from "marked";
const CONTENT_ROOT = path.join(process.cwd(), "content");
export type CompetitionSlug = "hands-on-challenge" | "ideathon" | "robo-soccer";
export type Competition = {
slug: CompetitionSlug;
name: string;
short: string;
tagline: string;
format: string;
durationDays?: number;
ageGroups?: string[];
categories?: string[];
color?: "brand" | "accent" | "pink";
icon?: string;
order: number;
bodyHtml: string;
};
export type Winner = {
place: number;
team: string;
school?: string;
members?: number;
};
export type Team = {
name: string;
school?: string;
size?: number;
note?: string;
};
export type Testimonial = {
by: string;
org?: string;
quote: string;
};
export type Edition = {
slug: string;
competition: CompetitionSlug;
name: string;
status: "upcoming" | "past" | "announced";
startDate?: string;
endDate?: string;
city?: string;
venue?: string;
hero?: string;
stats?: {
teams?: number;
participants?: number;
volunteers?: number;
};
partners?: string[];
winners?: Winner[];
teams?: Team[];
photos?: string[];
testimonials?: Testimonial[];
summary?: string;
bodyHtml: string;
};
function readDir(dir: string): string[] {
const full = path.join(CONTENT_ROOT, dir);
if (!fs.existsSync(full)) return [];
return fs
.readdirSync(full)
.filter((f) => f.endsWith(".md"))
.map((f) => path.join(full, f));
}
function parseFile<T extends object>(
filePath: string,
): { data: T; bodyHtml: string } {
const raw = fs.readFileSync(filePath, "utf8");
const parsed = matter(raw);
const bodyHtml = parsed.content.trim()
? (marked.parse(parsed.content, { async: false }) as string)
: "";
return { data: parsed.data as T, bodyHtml };
}
export function getCompetitions(): Competition[] {
const files = readDir("competitions");
const items = files.map((fp) => {
const { data, bodyHtml } = parseFile<Omit<Competition, "bodyHtml">>(fp);
return { ...data, bodyHtml };
});
return items.sort((a, b) => (a.order ?? 99) - (b.order ?? 99));
}
export function getCompetition(slug: string): Competition | null {
return getCompetitions().find((c) => c.slug === slug) ?? null;
}
export function getEditions(): Edition[] {
const files = readDir("editions");
const items = files.map((fp) => {
const { data, bodyHtml } = parseFile<Omit<Edition, "bodyHtml">>(fp);
return { ...data, bodyHtml };
});
return items.sort((a, b) => {
const ad = a.startDate ?? "0000";
const bd = b.startDate ?? "0000";
return bd.localeCompare(ad);
});
}
export function getEdition(slug: string): Edition | null {
return getEditions().find((e) => e.slug === slug) ?? null;
}
export function getEditionsFor(comp: CompetitionSlug): Edition[] {
return getEditions().filter((e) => e.competition === comp);
}
export function getUpcomingEdition(): Edition | null {
return (
getEditions().find((e) => e.status === "upcoming" || e.status === "announced") ?? null
);
}
export function getTotalStats(): {
editions: number;
teams: number;
participants: number;
volunteers: number;
} {
const past = getEditions().filter((e) => e.status === "past");
return past.reduce(
(acc, e) => ({
editions: acc.editions + 1,
teams: acc.teams + (e.stats?.teams ?? 0),
participants: acc.participants + (e.stats?.participants ?? 0),
volunteers: acc.volunteers + (e.stats?.volunteers ?? 0),
}),
{ editions: 0, teams: 0, participants: 0, volunteers: 0 },
);
}
export function formatDateRange(start?: string, end?: string): string {
if (!start) return "Tarih yakında açıklanacak";
const s = new Date(start);
const months = [
"Ocak",
"Şubat",
"Mart",
"Nisan",
"Mayıs",
"Haziran",
"Temmuz",
"Ağustos",
"Eylül",
"Ekim",
"Kasım",
"Aralık",
];
if (!end || end === start) {
return `${s.getUTCDate()} ${months[s.getUTCMonth()]} ${s.getUTCFullYear()}`;
}
const e = new Date(end);
if (s.getUTCMonth() === e.getUTCMonth() && s.getUTCFullYear() === e.getUTCFullYear()) {
return `${s.getUTCDate()}${e.getUTCDate()} ${months[s.getUTCMonth()]} ${s.getUTCFullYear()}`;
}
return `${s.getUTCDate()} ${months[s.getUTCMonth()]} ${e.getUTCDate()} ${months[e.getUTCMonth()]} ${e.getUTCFullYear()}`;
}

25
src/lib/reviews.ts Normal file
View File

@@ -0,0 +1,25 @@
export type Review = {
quote: string;
by: string;
role?: string;
org?: string;
edition?: string;
};
export const reviews: Review[] = [
{
quote:
"Impact Hands-On Challenge Çemberlitaş Regional 2 gün boyunca 16 robotik takımına, 150'den fazla katılımcıya, mentörlere ve eğitimcilere kapılarını açtı. Impact, eğitim programları ve yarışmalarıyla Türkiye'de STEM ekosisteminin her geçen gün gelişmesini sağlıyor. İkinci kez katıldığımız ve keyif aldığımız bu organizasyonu düzenleyen ekibi kutlarız.",
by: "M. Buğrahan Kılıç",
org: "Sultangazi Bilim ve İnovasyon Merkezi",
edition: "Çemberlitaş Regional 2026",
},
];
export const photos = {
cemberlitas: Array.from({ length: 40 }, (_, i) => ({
src: `/photos/cemberlitas-hands-on/${String(i + 1).padStart(2, "0")}.jpg`,
w: i + 1 === 28 ? 1600 : 1600,
h: i + 1 === 28 ? 2850 : 898,
})),
};