diff --git a/README.md b/README.md index cecbf10..e71cc32 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,83 @@ -# ๐ŸŽฎ TIPILAN +# TipiLAN -**TIPILAN** is the official web platform for the TipiLAN LAN event โ€” built using [Next.js](https://nextjs.org) and powered by the lightning-fast [Bun](https://bun.sh) runtime. +Official website for **TipiLAN** โ€” Estonia's largest student-organised LAN event. -> โš ๏ธ This is a work in progress! Contributions welcome. +> Work in progress. Event date: **September 11, 2026**. --- -## ๐Ÿš€ Features +## Tech Stack -- Event information & schedule -- Stream -- Responsive dark-themed UI -- Built with Next.js, styled with Tailwind (if applicable), powered by Bun +- [Next.js 16](https://nextjs.org/) โ€” App Router, SSG, Turbopack +- [React 19](https://react.dev/) +- [TypeScript 5](https://www.typescriptlang.org/) +- [Tailwind CSS 4](https://tailwindcss.com/) +- [next-intl](https://next-intl.dev/) โ€” ET / EN localisation +- [react-icons](https://react-icons.github.io/react-icons/) + +Requires **Node.js โ‰ฅ 22**. --- -## ๐Ÿ“ฆ Tech Stack - -- [Next.js](https://nextjs.org/) -- [Bun](https://bun.sh/) -- [TypeScript](https://www.typescriptlang.org/) -- [React](https://reactjs.org/) -- [Tailwind CSS](https://tailwindcss.com/) *(if used)* - ---- - -## ๐Ÿ› ๏ธ Setup Instructions - -๐Ÿ“– See [`docs/SETUP.md`](./docs/SETUP.md) for a full setup guide on: - -- โœ… Windows (native) -- ๐Ÿง Windows with WSL -- ๐Ÿง Linux (Ubuntu, Arch, Fedora, etc.) - ---- - -## ๐Ÿงช Scripts +## Quickstart ```bash -bun dev # Run development server -bun run build # Build for production -bun start # Start production server +npm install +npm run dev +``` -bun test # Run tests (if configured) -bun run lint # Run linter (if configured) +Open [http://localhost:3000](http://localhost:3000) โ€” redirects to `/et` by default. + +--- + +## Scripts + +```bash +npm run dev # Dev server (Turbopack, localhost:3000) +npm run build # Production build (outputs SSG for /et and /en) +npm start # Serve production build +npm run lint:fix # ESLint with auto-fix +``` + +--- + +## Project Structure + +``` +src/ + app/[locale]/ # Locale-based pages (et, en) + components/teaser/ # HeroSection, CarouselSection, EndSection, Footer + components/ # LanguageSwitcher, TeaserPage + i18n/ # next-intl routing + request config + lib/ # blurPlaceholders.ts (auto-generated) + proxy.ts # next-intl routing middleware + +translations/ # et.json, en.json +public/images/ + backgrounds/ # Full-viewport background WebPs + heros/ # Character/hero image WebPs + flags/ # Language switcher SVGs + original/ # Original PNG sources (not served) +scripts/ # Image processing utilities +``` + +--- + +## Image Utilities + +When adding or replacing images, re-run: + +```bash +# Convert new PNGs โ†’ WebP (quality 85) +node scripts/convert-images.mjs + +# Regenerate blur placeholders for all images +node scripts/gen-blur-placeholders.mjs +``` + +--- + +## Localisation + +Translation files live in `translations/et.json` and `translations/en.json`. +Routing is handled by `src/proxy.ts` โ€” `/` redirects to `/et` (default locale). diff --git a/next.config.ts b/next.config.ts index 58cef1b..d99df7f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,10 @@ import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); -const nextConfig: NextConfig = {}; +const nextConfig: NextConfig = { + images: { + qualities: [75, 90], + }, +}; export default withNextIntl(nextConfig); diff --git a/public/images/backgrounds/compete_teaser.webp b/public/images/backgrounds/compete_teaser.webp new file mode 100644 index 0000000..3cb18e6 Binary files /dev/null and b/public/images/backgrounds/compete_teaser.webp differ diff --git a/public/images/backgrounds/explore_teaser.webp b/public/images/backgrounds/explore_teaser.webp new file mode 100644 index 0000000..fc91193 Binary files /dev/null and b/public/images/backgrounds/explore_teaser.webp differ diff --git a/public/images/backgrounds/hero_teaser.webp b/public/images/backgrounds/hero_teaser.webp new file mode 100644 index 0000000..621d30a Binary files /dev/null and b/public/images/backgrounds/hero_teaser.webp differ diff --git a/public/images/backgrounds/play_teaser.webp b/public/images/backgrounds/play_teaser.webp new file mode 100644 index 0000000..6601f5c Binary files /dev/null and b/public/images/backgrounds/play_teaser.webp differ diff --git a/public/images/backgrounds/sponsors_teaser.webp b/public/images/backgrounds/sponsors_teaser.webp new file mode 100644 index 0000000..5efc751 Binary files /dev/null and b/public/images/backgrounds/sponsors_teaser.webp differ diff --git a/public/images/backgrounds/tickets_teaser.webp b/public/images/backgrounds/tickets_teaser.webp new file mode 100644 index 0000000..a1fedeb Binary files /dev/null and b/public/images/backgrounds/tickets_teaser.webp differ diff --git a/public/images/flag-en.svg b/public/images/flags/flag-en.svg similarity index 100% rename from public/images/flag-en.svg rename to public/images/flags/flag-en.svg diff --git a/public/images/flag-et.svg b/public/images/flags/flag-et.svg similarity index 100% rename from public/images/flag-et.svg rename to public/images/flags/flag-et.svg diff --git a/public/images/heros/compete_hero.webp b/public/images/heros/compete_hero.webp new file mode 100644 index 0000000..3cd1bec Binary files /dev/null and b/public/images/heros/compete_hero.webp differ diff --git a/public/images/heros/explore_hero.webp b/public/images/heros/explore_hero.webp new file mode 100644 index 0000000..c97efb6 Binary files /dev/null and b/public/images/heros/explore_hero.webp differ diff --git a/public/images/heros/play_hero.webp b/public/images/heros/play_hero.webp new file mode 100644 index 0000000..2344d88 Binary files /dev/null and b/public/images/heros/play_hero.webp differ diff --git a/public/images/compete_hero.png b/public/images/original/compete_hero.png similarity index 100% rename from public/images/compete_hero.png rename to public/images/original/compete_hero.png diff --git a/public/images/compete_teaser.png b/public/images/original/compete_teaser.png similarity index 100% rename from public/images/compete_teaser.png rename to public/images/original/compete_teaser.png diff --git a/public/images/explore_hero.png b/public/images/original/explore_hero.png similarity index 100% rename from public/images/explore_hero.png rename to public/images/original/explore_hero.png diff --git a/public/images/explore_teaser.png b/public/images/original/explore_teaser.png similarity index 100% rename from public/images/explore_teaser.png rename to public/images/original/explore_teaser.png diff --git a/public/images/hero_teaser.png b/public/images/original/hero_teaser.png similarity index 100% rename from public/images/hero_teaser.png rename to public/images/original/hero_teaser.png diff --git a/public/images/play_hero.png b/public/images/original/play_hero.png similarity index 100% rename from public/images/play_hero.png rename to public/images/original/play_hero.png diff --git a/public/images/play_teaser.png b/public/images/original/play_teaser.png similarity index 100% rename from public/images/play_teaser.png rename to public/images/original/play_teaser.png diff --git a/public/images/sponsors_teaser.png b/public/images/original/sponsors_teaser.png similarity index 100% rename from public/images/sponsors_teaser.png rename to public/images/original/sponsors_teaser.png diff --git a/public/images/tickets_teaser.png b/public/images/original/tickets_teaser.png similarity index 100% rename from public/images/tickets_teaser.png rename to public/images/original/tickets_teaser.png diff --git a/scripts/convert-images.mjs b/scripts/convert-images.mjs new file mode 100644 index 0000000..25020ae --- /dev/null +++ b/scripts/convert-images.mjs @@ -0,0 +1,27 @@ +import sharp from 'sharp'; +import { readdir, unlink } from 'fs/promises'; +import { join, extname, basename } from 'path'; + +const INPUT_DIR = 'public/images'; +const QUALITY = 85; + +const files = await readdir(INPUT_DIR); +const pngs = files.filter(f => extname(f).toLowerCase() === '.png'); + +console.log(`Converting ${pngs.length} PNG(s) to WebP at quality ${QUALITY}...\n`); + +for (const file of pngs) { + const input = join(INPUT_DIR, file); + const output = join(INPUT_DIR, basename(file, '.png') + '.webp'); + + const { width, height, size: inSize } = await sharp(input).metadata(); + await sharp(input).webp({ quality: QUALITY }).toFile(output); + + const { size: outSize } = await sharp(output).metadata(); + const saved = (((inSize - outSize) / inSize) * 100).toFixed(1); + + console.log(`${file} (${width}x${height})`); + console.log(` ${(inSize/1024).toFixed(0)} KB โ†’ ${(outSize/1024).toFixed(0)} KB (${saved}% smaller)\n`); +} + +console.log('Done.'); diff --git a/scripts/gen-blur-placeholders.mjs b/scripts/gen-blur-placeholders.mjs new file mode 100644 index 0000000..8b643b4 --- /dev/null +++ b/scripts/gen-blur-placeholders.mjs @@ -0,0 +1,43 @@ +import sharp from 'sharp'; +import { readdir, writeFile } from 'fs/promises'; +import { join, extname, basename } from 'path'; + +const INPUT_DIR = 'public/images'; +const OUTPUT_FILE = 'src/lib/blurPlaceholders.ts'; +const THUMB_WIDTH = 20; + +async function processDir(dir, prefix = '') { + const entries = await readdir(dir, { withFileTypes: true }); + const result = {}; + + for (const entry of entries) { + if (entry.name === 'original') continue; + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + const sub = await processDir(fullPath, prefix + entry.name + '/'); + Object.assign(result, sub); + } else if (['.webp', '.png', '.jpg'].includes(extname(entry.name).toLowerCase())) { + const buffer = await sharp(fullPath) + .resize(THUMB_WIDTH) + .webp({ quality: 30 }) + .toBuffer(); + const b64 = `data:image/webp;base64,${buffer.toString('base64')}`; + const key = prefix + basename(entry.name, extname(entry.name)); + result[key] = b64; + console.log(` ${key} โ†’ ${b64.length} chars`); + } + } + return result; +} + +console.log('Generating blur placeholders...\n'); +const placeholders = await processDir(INPUT_DIR); + +const lines = Object.entries(placeholders) + .map(([k, v]) => ` "${k}": "${v}",`) + .join('\n'); + +const output = `// Auto-generated by scripts/gen-blur-placeholders.mjs โ€” do not edit manually\nexport const BLUR_PLACEHOLDERS: Record = {\n${lines}\n};\n`; + +await writeFile(OUTPUT_FILE, output, 'utf8'); +console.log(`\nWrote ${Object.keys(placeholders).length} placeholders to ${OUTPUT_FILE}`); diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index ebdd073..6306f5f 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,5 +1,10 @@ import { NextIntlClientProvider } from "next-intl"; import { setRequestLocale, getMessages } from "next-intl/server"; +import { routing } from "@/i18n/routing"; + +export function generateStaticParams() { + return routing.locales.map((locale) => ({ locale })); +} export default async function LocaleLayout({ children, diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index cb1a91e..3574f82 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -28,7 +28,7 @@ export default function LanguageSwitcher() { aria-label="Switch language" > {locale {/* Content โ€” top padding accounts for the floating heading */}
{/* Text content */} @@ -51,11 +59,15 @@ function CarouselSlideComponent({ transform: isActive ? "translateY(0)" : "translateY(110%)", }} > - {/* eslint-disable-next-line @next/next/no-img-element */} -
@@ -102,15 +114,18 @@ export default function CarouselSection({ sectionRef }: CarouselSectionProps) { {/* Mobile: stacked slides, no hero images, no transitions */}
{SLIDES.map((slide, i) => ( -
+
-
+

{t(slide.titleKey)}

{t(slide.descKey)}

diff --git a/src/components/teaser/EndSection.tsx b/src/components/teaser/EndSection.tsx index b700f63..e524650 100644 --- a/src/components/teaser/EndSection.tsx +++ b/src/components/teaser/EndSection.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import { useTranslations } from "next-intl"; import { useCountUp } from "@/hooks/useCountUp"; +import { BLUR_PLACEHOLDERS } from "@/lib/blurPlaceholders"; function formatK(n: number): string { if (n < 1000) return String(n); @@ -50,13 +51,16 @@ export default function EndSection() { {/* Tickets side */}
-
+
{/* Ticket stats */}
@@ -80,13 +84,16 @@ export default function EndSection() { {/* Sponsors side */}
-
+
{/* Sponsor stats */}
diff --git a/src/components/teaser/Footer.tsx b/src/components/teaser/Footer.tsx index fcebfba..26df8d1 100644 --- a/src/components/teaser/Footer.tsx +++ b/src/components/teaser/Footer.tsx @@ -16,7 +16,7 @@ export default function Footer() { const t = useTranslations(); return ( -