Prod ready, heavily optimized images with automatic scripts
105
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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
BIN
public/images/backgrounds/compete_teaser.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/images/backgrounds/explore_teaser.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/images/backgrounds/hero_teaser.webp
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/images/backgrounds/play_teaser.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
public/images/backgrounds/sponsors_teaser.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
public/images/backgrounds/tickets_teaser.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 574 B After Width: | Height: | Size: 574 B |
BIN
public/images/heros/compete_hero.webp
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
public/images/heros/explore_hero.webp
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
public/images/heros/play_hero.webp
Normal file
|
After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1015 KiB After Width: | Height: | Size: 1015 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
27
scripts/convert-images.mjs
Normal file
@@ -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.');
|
||||
43
scripts/gen-blur-placeholders.mjs
Normal file
@@ -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<string, string> = {\n${lines}\n};\n`;
|
||||
|
||||
await writeFile(OUTPUT_FILE, output, 'utf8');
|
||||
console.log(`\nWrote ${Object.keys(placeholders).length} placeholders to ${OUTPUT_FILE}`);
|
||||
@@ -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,
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function LanguageSwitcher() {
|
||||
aria-label="Switch language"
|
||||
>
|
||||
<Image
|
||||
src={`/images/flag-${locale === "et" ? "en" : "et"}.svg`}
|
||||
src={`/images/flags/flag-${locale === "et" ? "en" : "et"}.svg`}
|
||||
alt={locale === "et" ? "Switch to English" : "Vaheta eesti keelele"}
|
||||
width={40}
|
||||
height={30}
|
||||
|
||||
@@ -4,6 +4,11 @@ import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { SLIDES, type CarouselSlide } from "./constants";
|
||||
import { BLUR_PLACEHOLDERS } from "@/lib/blurPlaceholders";
|
||||
|
||||
function blurKey(src: string) {
|
||||
return src.replace('/images/', '').replace(/\.\w+$/, '');
|
||||
}
|
||||
|
||||
function CarouselSlideComponent({
|
||||
slide,
|
||||
@@ -23,13 +28,16 @@ function CarouselSlideComponent({
|
||||
src={slide.bgImage}
|
||||
alt=""
|
||||
fill
|
||||
unoptimized
|
||||
placeholder="blur"
|
||||
blurDataURL={BLUR_PLACEHOLDERS[blurKey(slide.bgImage)]}
|
||||
sizes="100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
|
||||
{/* Content — top padding accounts for the floating heading */}
|
||||
<div
|
||||
className={`relative z-[1] flex h-full gap-8 xl:gap-16 pt-[120px] px-8 xl:px-16 ${isLeft ? "items-end" : "items-end flex-row-reverse"
|
||||
className={`relative z-1 flex h-full gap-8 xl:gap-16 pt-[120px] px-8 xl:px-16 ${isLeft ? "items-end" : "items-end flex-row-reverse"
|
||||
}`}
|
||||
>
|
||||
{/* Text content */}
|
||||
@@ -51,11 +59,15 @@ function CarouselSlideComponent({
|
||||
transform: isActive ? "translateY(0)" : "translateY(110%)",
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
<Image
|
||||
src={slide.heroImage}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
fill
|
||||
unoptimized
|
||||
placeholder="blur"
|
||||
blurDataURL={BLUR_PLACEHOLDERS[blurKey(slide.heroImage)]}
|
||||
sizes="(min-width: 1280px) 40vw, 35vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,15 +114,18 @@ export default function CarouselSection({ sectionRef }: CarouselSectionProps) {
|
||||
{/* Mobile: stacked slides, no hero images, no transitions */}
|
||||
<div className="lg:hidden">
|
||||
{SLIDES.map((slide, i) => (
|
||||
<div key={i} className="relative w-full overflow-hidden border-t-4 border-primary-50">
|
||||
<div key={i} className="relative w-full h-[60vw] min-h-[320px] overflow-hidden border-t-4 border-primary-50">
|
||||
<Image
|
||||
src={slide.bgImage}
|
||||
alt=""
|
||||
fill
|
||||
unoptimized
|
||||
placeholder="blur"
|
||||
blurDataURL={BLUR_PLACEHOLDERS[blurKey(slide.bgImage)]}
|
||||
sizes="100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-[1] flex flex-col gap-4 justify-end h-full p-6 pb-12">
|
||||
<div className="relative z-1 flex flex-col gap-4 justify-end h-full p-6 pb-12">
|
||||
<h3 className="text-h1 text-text-light text-shadow-teaser">{t(slide.titleKey)}</h3>
|
||||
<p className="text-p-lg text-text-light max-w-[500px]">{t(slide.descKey)}</p>
|
||||
</div>
|
||||
|
||||
@@ -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 */}
|
||||
<div className="relative w-1/2 overflow-hidden min-h-dvh">
|
||||
<Image
|
||||
src="/images/tickets_teaser.png"
|
||||
src="/images/backgrounds/tickets_teaser.webp"
|
||||
alt=""
|
||||
fill
|
||||
unoptimized
|
||||
placeholder="blur"
|
||||
blurDataURL={BLUR_PLACEHOLDERS["backgrounds/tickets_teaser"]}
|
||||
sizes="(min-width: 1280px) 50vw, 100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16">
|
||||
<div className="relative z-1 flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16">
|
||||
{/* Ticket stats */}
|
||||
<div className="flex gap-6 xl:gap-12 text-center text-text-light">
|
||||
<CountUpStat end={0} suffix="€" label={t("teaser.tickets.earlyVisitor")} />
|
||||
@@ -80,13 +84,16 @@ export default function EndSection() {
|
||||
{/* Sponsors side */}
|
||||
<div className="relative w-1/2 overflow-hidden min-h-dvh">
|
||||
<Image
|
||||
src="/images/sponsors_teaser.png"
|
||||
src="/images/backgrounds/sponsors_teaser.webp"
|
||||
alt=""
|
||||
fill
|
||||
unoptimized
|
||||
placeholder="blur"
|
||||
blurDataURL={BLUR_PLACEHOLDERS["backgrounds/sponsors_teaser"]}
|
||||
sizes="(min-width: 1280px) 50vw, 100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16">
|
||||
<div className="relative z-1 flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16">
|
||||
{/* Sponsor stats */}
|
||||
<div className="flex gap-6 xl:gap-12 text-center text-text-light">
|
||||
<CountUpStat end={900} suffix="+" label={t("teaser.sponsors.visitors")} />
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function Footer() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<footer className="bg-bg-dark w-full border-t-4 border-primary-50">
|
||||
<footer className="bg-bg-dark w-full border-t-4 border-primary-50 lg:border-t-0">
|
||||
{/* Mobile: only social icons */}
|
||||
<div className="flex lg:hidden items-center justify-center gap-6 py-8 px-6 flex-wrap">
|
||||
{SOCIAL_LINKS.map(({ icon: Icon, href, label }) => (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import { useCountdown } from "@/hooks/useCountdown";
|
||||
import { EVENT_DATE } from "./constants";
|
||||
import { BLUR_PLACEHOLDERS } from "@/lib/blurPlaceholders";
|
||||
|
||||
interface HeroSectionProps {
|
||||
onScrollDown: () => void;
|
||||
@@ -19,9 +20,12 @@ export default function HeroSection({ onScrollDown }: HeroSectionProps) {
|
||||
<section className="relative h-dvh w-full overflow-hidden">
|
||||
{/* Background */}
|
||||
<Image
|
||||
src="/images/hero_teaser.png"
|
||||
src="/images/backgrounds/hero_teaser.webp"
|
||||
alt=""
|
||||
fill
|
||||
unoptimized
|
||||
placeholder="blur"
|
||||
blurDataURL={BLUR_PLACEHOLDERS["backgrounds/hero_teaser"]}
|
||||
className="object-cover pointer-events-none"
|
||||
sizes="100vw"
|
||||
priority
|
||||
|
||||
@@ -13,22 +13,22 @@ export const SLIDES: CarouselSlide[] = [
|
||||
{
|
||||
titleKey: "teaser.compete.title",
|
||||
descKey: "teaser.compete.description",
|
||||
bgImage: "/images/compete_teaser.png",
|
||||
heroImage: "/images/compete_hero.png",
|
||||
bgImage: "/images/backgrounds/compete_teaser.webp",
|
||||
heroImage: "/images/heros/compete_hero.webp",
|
||||
layout: "left",
|
||||
},
|
||||
{
|
||||
titleKey: "teaser.play.title",
|
||||
descKey: "teaser.play.description",
|
||||
bgImage: "/images/play_teaser.png",
|
||||
heroImage: "/images/play_hero.png",
|
||||
bgImage: "/images/backgrounds/play_teaser.webp",
|
||||
heroImage: "/images/heros/play_hero.webp",
|
||||
layout: "right",
|
||||
},
|
||||
{
|
||||
titleKey: "teaser.explore.title",
|
||||
descKey: "teaser.explore.description",
|
||||
bgImage: "/images/explore_teaser.png",
|
||||
heroImage: "/images/explore_hero.png",
|
||||
bgImage: "/images/backgrounds/explore_teaser.webp",
|
||||
heroImage: "/images/heros/explore_hero.webp",
|
||||
layout: "left",
|
||||
},
|
||||
];
|
||||
|
||||
12
src/lib/blurPlaceholders.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Auto-generated by scripts/gen-blur-placeholders.mjs — do not edit manually
|
||||
export const BLUR_PLACEHOLDERS: Record<string, string> = {
|
||||
"backgrounds/compete_teaser": "data:image/webp;base64,UklGRkQAAABXRUJQVlA4IDgAAADwAgCdASoUAAsAPxFysFAsJqSisAgBgCIJZQC7AC0kAAD+7wHPoVxbdBCX00WbjgD9Z4tx1CAAAA==",
|
||||
"backgrounds/explore_teaser": "data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADQAwCdASoUAAsAPxFysFAsJqSisAgBgCIJZQCdACPtdR6HHnLZHwAA/uk16hD6XlyktfBMGDYcaSECd8XN57v+wAA=",
|
||||
"backgrounds/hero_teaser": "data:image/webp;base64,UklGRjwAAABXRUJQVlA4IDAAAADwAgCdASoUAAsAPxFysFAsJqSisAgBgCIJZQCw7C0kAAD+7wd2LOZnmb7bPjFHAAA=",
|
||||
"backgrounds/play_teaser": "data:image/webp;base64,UklGRkYAAABXRUJQVlA4IDoAAAAQAwCdASoUAAsAPxFysFAsJqSisAgBgCIJZQAAetFV2AAA/u7a/rkwOPegln3XZvJv4IXGbjFCgAAA",
|
||||
"backgrounds/sponsors_teaser": "data:image/webp;base64,UklGRkoAAABXRUJQVlA4ID4AAACwAgCdASoUAAsAPxFysVAsJqSisAgBgCIJZQAAeyAA/u8NKV4mJzYIQ5TdP6QE3PTLq6L/cRRlp5DBY4AAAA==",
|
||||
"backgrounds/tickets_teaser": "data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAADwAgCdASoUAAsAPxFysFAsJqSisAgBgCIJZQCsADDWAAD+7wOPnse+a27m6AB0gO1dloAA",
|
||||
"heros/compete_hero": "data:image/webp;base64,UklGRmABAABXRUJQVlA4WAoAAAAQAAAAEwAAFAAAQUxQSLQAAAABgGrbtrJl/y4Nd/fEILkm90wjkUjQIJEZRLdKdHkAonukubsu5JP/ESJiAmTd5ZSpv2d4uM0huduWZ8tMOoHvJqnsA9YDRs0AfVIt8JBglPYJ1En533ARbeQYhGOfFH4KC24jxcNGnksagAIZZ84BbHfFLMHaaLUkR+8Lxjf8f6yR4r+we5WsROz3K/hub0yumVAo7DUUse+hqCAU1SFpCIHD0W3rsyNnaN/OSbtDheN2syRWUDgghgAAANADAJ0BKhQAFQA/EYC4VawoJSMoCAGAIglmAMiYGHsmEwuFEhEkYAD+66HVfxcO3w6PqL2pnKTu3Ng/jKAoDs8naMiWdzhP7r5h1yoBgzUT75Dx8f0hkv9pe7sM/bowsdteFBkLMihe2/KstbbBJ3JDP0lIY68rF2gLM5uQUOPBuAQ6AAAA",
|
||||
"heros/explore_hero": "data:image/webp;base64,UklGRnoBAABXRUJQVlA4WAoAAAAQAAAAEwAAFAAAQUxQSMcAAAABgGPb2rHn/orVpf6r2OxS22ZpZxC2nbSZwd/GNuZgG++NnvebQkRMADRNoUkWGI794pjJUCu5YayFXDcUsEkeFDtJ/vEREbkf/DtvFiJOKd/aBcvkh8Q1l//QcqDBbtN/UXc615GmfxJudagyAU8bEue02ANMViA9VWmVALWNKAk+F3YOeeUDjDrMEYMUZxfYDSD1wsXpXnKoM18AOWzIe5MeOQwAdqo36lb+MY9R9/HpugwAvJaopLmw9koTADj5+02K5QAAAFZQOCCMAAAA8AQAnQEqFAAVAD8RfLNULCekIygKqYAiCWwAAC6K7ktWusnkjyqNZvLtrf5IXSsgAP7Y10kJQQoMowu9tcKs37WMMYK0+ODOOnN6ITImiWwI2YSiqAiB1HVtiJn751d+7RynAhi3kIDVj/gRpYzPtuFjAKK95QkeE+vkcbEHeYc8lEOzfIUG9z3UwAA=",
|
||||
"heros/play_hero": "data:image/webp;base64,UklGRpgBAABXRUJQVlA4WAoAAAAQAAAAEwAAFAAAQUxQSNoAAAABgGPbtmnplG3brszKbJuhjSZUaER//KxGRbarWmDbtv3eCd5593chIiYAAECxKsVTS1EO2I15xJ+DydZSbzlFMTcUHWq4qFekgsU4RH7LlUgUIz2JQiaOMyVCpQs7UyfU94c3AdB40/dEIWdL6Y0FHXFfv7+CbjmBRl1Xp8oVPj2gMAOEUkR+nkf631Igv4bMuwoCkLIdOhIhf0z47AAAipo9+MOCh/6gvfiBzM3+OgoQiTLmKACAnyxJckz7PyhZxC8HAACrp03isuA0TsnLBki1CAlpryQPNFZQOCCYAAAAkAQAnQEqFAAVAD8RfrZVrCekoygIAYAiCWMAA+YY8jKFKQn7mN5HXGKuMKLAAPnN+XfRnezaXgxFG0vexSu324H8iTVzaqp6+GAnLm3Z0vYGqKNz9UAFLu/aVjnBei3xNpg0DHr4e2EMCNq6VGTYtaJn7sVI0C/bn2ttJ3P6x68okPFLtX69dR+UTOMhnz1zhE3rAwgAAAA=",
|
||||
};
|
||||
13
src/proxy.ts
@@ -1,17 +1,8 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
const intlMiddleware = createMiddleware(routing);
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
return intlMiddleware(request);
|
||||
}
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/(et|en)/:path*',
|
||||
'/((?!_next|_vercel|.*\\..*).*)'
|
||||
]
|
||||
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
|
||||
};
|
||||
|
||||