Compare commits

...

No commits in common. "78a2382fd0326987f7e50c1f5c2742732bc75222" and "1b9065bb094519e50cd0c0642c87b4ab3a2e3c95" have entirely different histories.

84 changed files with 12259 additions and 55 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
public/videos/** filter=lfs diff=lfs merge=lfs -text
source-videos/** filter=lfs diff=lfs merge=lfs -text

41
.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

49
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,49 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug client-side (Firefox)",
"type": "firefox",
"request": "launch",
"url": "http://localhost:3000",
"reAttach": true,
"pathMappings": [
{
"url": "webpack://_N_E",
"path": "${workspaceFolder}"
}
]
},
{
"name": "Next.js: debug full stack",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/next",
"runtimeArgs": [
"--inspect"
],
"skipFiles": [
"<node_internals>/**"
],
"serverReadyAction": {
"action": "debugWithEdge",
"killOnServerStop": true,
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}"
}
}
]
}

View file

@ -1,3 +1,56 @@
FROM nginx:1.27
FROM node:23-alpine3.20@sha256:3ac002b133fd3737c44b66f0d023f5d75c7a1ddec71954fa06486aed6aead888 AS base
COPY index.html ceboola.mp4 /usr/share/nginx/html/
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY . .
RUN npm ci --ignore-scripts
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# If using npm comment out above and use below instead
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ARG VERSION="local"
ENV VERSION=$VERSION
CMD echo "Version: $VERSION" && node server.js

View file

@ -1,13 +1,36 @@
# Deploying
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).
No CI yet, build and push locally:
## Getting Started
docker build -t git.orga.cebula.camp/infra/site:golden .
docker push git.orga.cebula.camp/infra/site:golden
First, run the development server:
No autodeploy yet, ping q3k when done.
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
To get access to the container registry, add an [access
token](https://git.orga.cebula.camp/user/settings/applications) with read/write
permissions to `package`. Then `docker login git.orga.cebula.camp` and use your
username and the token as the password.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
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.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [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.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
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.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

Binary file not shown.

21
components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/src/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/src/lib",
"hooks": "@/src/hooks"
},
"iconLibrary": "lucide"
}

16
eslint.config.mjs Normal file
View file

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View file

@ -1,44 +0,0 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>cebula.camp 2025</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" type="image/png" href="/favicon.png">
<style>
body {
font-family: arial, sans-serif;
font-size: 12px;
text-align: center;
margin-top: 100px;
background-color: #000;
color: #eee;
}
</style>
<h1>CEBULA.CAMP 2025 | REAKTYWACJA</h1>
<video loop autoplay muted style="height: 50vh;">
<source src="ceboola.mp4" type="video/mp4" />
</video>
<p style="font-size: 14px;">
Zjazd hakerów, miłośników open source, wolnych duchów. Organizowany przez hakerów dla hakerów. Będzie mate, będzie utopia, będzie chillera.
</p>
<p style="font-size: 14px;">
Spodziewaj się ciekawych prezentacji, dziwnych instalacji artystycznych i mnóstwa dyskusji. Możesz opowiedzieć o swoim projekcie, zademonstrować skonstruowane zabawki, albo wspólnie coś stworzyć podczas eventu.
</p>
<p style="margin-top: 5em;">
<b>Gdzie</b>: klub Łącznik, Tramwajowa 1-3, Wrocław, obok Hackerspace Wrocław
</p>
<p>
<b>Kiedy</b>: 28-31.08.2025 (chętnych do pomocy w przygotowaniach zapraszamy już na *Day 0* 27 sierpnia)
</p>
<p>
<b>Bilety</b>: soon
</p>
<p>
<b>Nocleg</b>: w duchu prawdziwego campu przygotowujemy ogrodzone miejsce do rozbicia namiotu z toaletami i prysznicem. Ilość miejsc namiotowych ograniczona, obowiązuje kolejność rezerwacji. Jest także możliwość zakwaterowania we własnym zakresie w pobliskim hotelu lub akademikach.
</p>
<p>
<b>Wyżywienie</b>: we własnym zakresie, w okolicy dostępne są knajpy z dowozem, wieczory planujemy umilić wspólnym grillowaniem.
</p>
<p>
<b>Kontakt</b>: orga@cebula.camp
</p>

7
next.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

10538
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "cebula-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"translations:extract": "lingui extract-template",
"translations:compile": "lingui compile",
"translations:sync": "lingui extract --overwrite && lingui compile",
"translations:sync_and_purge": "lingui extract --overwrite --clean && lingui compile"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.10",
"@lingui/core": "^5.1.2",
"@lingui/macro": "^5.1.2",
"@lingui/react": "^5.1.2",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.3.1",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"lucide-react": "^0.474.0",
"make-plural": "^7.4.0",
"negotiator": "^1.0.0",
"next": "15.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0-rc.2",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@lingui/loader": "^5.1.2",
"@lingui/swc-plugin": "^5.0.2",
"@lingui/vite-plugin": "^5.1.2",
"@tailwindcss/postcss": "^4.0.0",
"@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"postcss": "^8",
"tailwindcss": "^4.0.0",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/fonts/jgs7.ttf Normal file

Binary file not shown.

BIN
public/fonts/jgs7.woff Normal file

Binary file not shown.

BIN
public/fonts/jgs7.woff2 Normal file

Binary file not shown.

BIN
public/jgs7.ttf Normal file

Binary file not shown.

21
public/site.webmanifest Normal file
View file

@ -0,0 +1,21 @@
{
"name": "CebulaCamp",
"short_name": "Cebula",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
public/videos/ceboola_gradient-white_full.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_full.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_full.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_hd.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_hd.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_hd.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_mobile.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_mobile.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_mobile.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_tablet.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_tablet.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_tablet.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_twok.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_twok.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_twok.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_uhd.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_uhd.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient-white_uhd.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_full.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_full.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_full.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_hd.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_hd.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_hd.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_mobile.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_mobile.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_mobile.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_tablet.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_tablet.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_tablet.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_twok.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_twok.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_twok.webm (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_uhd.mp4 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_uhd.ogv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/videos/ceboola_gradient_uhd.webm (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

94
scripts/reencode-videos.sh Executable file
View file

@ -0,0 +1,94 @@
#!/bin/bash
# Define directories
SOURCE_DIR="./source-videos"
DEST_DIR="./public/videos"
# Define sizes and names
SIZES=(480 720 1080 1440 2160)
NAMES=("mobile" "tablet" "hd" "twok" "uhd")
# Create destination directory if it doesn't exist
mkdir -p "$DEST_DIR"
for video in "$SOURCE_DIR"/*.mkv; do
if [ -f "$video" ]; then
filename=$(basename "$video" .mkv)
echo "Processing: $filename"
# Get video dimensions
width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "$video")
height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$video")
# Process each size
for i in "${!SIZES[@]}"; do
size="${SIZES[$i]}"
name="${NAMES[$i]}"
echo "Debug: Processing $name with height=$size"
if [ "$size" -le "$height" ]; then
echo "Processing size $name (${size}p)"
# MP4
ffmpeg -n -i "$video" \
-c:v libx264 \
-vf "scale=-1:${size}" \
-preset slow \
-crf 23 \
-an \
"${DEST_DIR}/${filename}_${name}.mp4"
# WebM
ffmpeg -n -i "$video" \
-c:v libvpx-vp9 \
-deadline good \
-cpu-used 2 \
-row-mt 1 \
-threads 8 \
-vf "scale=-1:${size}" \
-quality good \
-an \
-crf 20 \
"${DEST_DIR}/${filename}_${name}.webm"
# Ogg
ffmpeg -n -i "$video" \
-c:v libtheora \
-q:v 5 \
-vf "scale=-1:${size}" \
-an \
"${DEST_DIR}/${filename}_${name}.ogv"
fi
done
# Create original resolution version
echo "Creating original resolution version"
ffmpeg -n -i "$video" \
-c:v libx264 \
-preset slow \
-an \
"${DEST_DIR}/${filename}_full.mp4"
ffmpeg -n -i "$video" \
-c:v libvpx-vp9 \
-deadline good \
-cpu-used 2 \
-row-mt 1 \
-threads 8 \
-quality good \
-an \
"${DEST_DIR}/${filename}_full.webm"
ffmpeg -n -i "$video" \
-c:v libtheora \
-q:v 5 \
-an \
"${DEST_DIR}/${filename}_full.ogv"
echo "Completed processing: $filename"
echo "----------------------------"
fi
done
echo "All videos have been processed."

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,37 @@
'use client'
import { useEffect } from "react"
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div className="flex h-full w-full items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold tracking-tight text-black sm:text-8xl">
{error.name}
</h1>
<p className="mt-4 text-lg text-gray-500">{error.message}</p>
<div className="mt-6">
<button
type="button"
className="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-xs hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:text-sm"
onClick={reset}
>
Try again
</button>
</div>
</div>
</div>
)
}

115
src/app/[locale]/layout.tsx Normal file
View file

@ -0,0 +1,115 @@
import "../../globals.css";
import type React from "react";
import { getLocale, Lang } from "@/i18n/locales";
import Head from 'next/head';
import { ThemeProvider } from "@/components/providers";
import { translations } from "@/i18n/translations";
import { Oxanium } from "next/font/google";
import { headers } from "next/headers";
const oxanium = Oxanium({ subsets: ["latin-ext"] })
import type { Metadata } from 'next';
type Props = {
params: Promise<{ locale: Lang }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export async function generateMetadata(
{ params }: Props,
): Promise<Metadata> {
// read route params
const locale = (await params).locale
const currentLang = getLocale(locale);
const t = translations[currentLang];
return {
title: "CebulaCamp",
description: "An amazing gathering of hackers and open source enthusiasts.",
icons: {
icon: [
{ url: '/favicon-96x96.png', type: 'image/png' },
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon.ico', type: 'image/x-icon' }
],
apple: '/apple-touch-icon.png',
},
manifest: '/site.webmanifest',
other: {
'apple-mobile-web-app-title': t.siteTitle,
},
openGraph: {
title: t.siteTitle,
description: 'An amazing gathering of hackers and open source enthusiasts.',
url: 'https://cebula.camp',
images: [
{
url: 'https://cebula.camp/web-app-manifest-512x512.png',
width: 512,
height: 512,
alt: t.siteTitle,
},
],
siteName: t.siteTitle,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'CebulaCamp',
description: 'An amazing gathering of hackers and open source enthusiasts.',
images: ['https://cebula.camp/web-app-manifest-512x512.png'],
},
};
}
export default async function RootLayout({
children,
params
}: {
children: React.ReactNode
params: Promise<{ locale: Lang }>
}) {
const [{ locale }, head] = await Promise.all([
params,
headers(),
]);
const preferedTheme = head.get("Sec-CH-Prefers-Color-Scheme")?.toLowerCase();
const supportedThemes = ["dark", "light"];
const isDarkOrLight =
preferedTheme && supportedThemes.includes(preferedTheme);
const currentLang = getLocale(locale);
const defaultTheme = isDarkOrLight ? preferedTheme : "dark";
const t = translations[currentLang];
return (
<html lang={currentLang} className={`${oxanium.className} ${defaultTheme}`}>
<Head>
<title>{t.siteTitle}</title>
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="CebulaCamp" />
<link rel="manifest" href="/site.webmanifest" />
</Head>
<body className="bg-background text:foreground antialiased">
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
)
}

View file

@ -0,0 +1,4 @@
export default function Loading() {
return <div>Loading...</div>
}

View file

@ -0,0 +1,5 @@
export default function NotFound() {
return <div>Not Found</div>
}

17
src/app/[locale]/page.tsx Normal file
View file

@ -0,0 +1,17 @@
import LandingPage from "@/components/landing-page";
import { getLocale, Lang } from "@/i18n/locales";
import { translations } from "@/i18n/translations";
export default async function Home(
{ params }
:
{ params: Promise<{ locale: Lang }> }
) {
const { locale } = await params
const currentLocale = getLocale(locale)
const t = translations[currentLocale];
return <LandingPage t={t} />
}

View file

@ -0,0 +1,29 @@
import "leaflet/dist/leaflet.css"
import "leaflet-defaulticon-compatibility"
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"
import { Translations } from "@/i18n/translations"
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet"
export default function Map({ t }: {
t: Translations
}) {
return (
<div className='h-screen max-h-[400px] pt-8 relative mx-auto max-w-3xl'>
{/* @ts-expect-error dragging IS a valid prop. */}
<MapContainer dragging={false} center={[51.105173, 17.087157]} zoom={40} scrollWheelZoom={false} className="h-full w-full">
{/* @ts-expect-error attribution IS a valid prop. */}
<TileLayer attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[51.105173, 17.087157]}>
<Popup>
{t.where.location}
</Popup>
</Marker>
</MapContainer>
</div>
)
}

View file

@ -0,0 +1,188 @@
"use client"
import dynamic from 'next/dynamic';
import { Nav } from "@/components/nav";
import { Translations } from "@/i18n/translations";
import { cn } from "@/lib/utils";
import { ReactElement, useEffect, useRef } from "react";
import { useTheme } from "./providers";
import { Skeleton } from './ui/skeleton';
function Section({
id,
title,
paragraphs,
after
}: {
id: string
title: string;
paragraphs: ReactElement;
after?: ReactElement;
}) {
return (<section id={id} className="bg-background">
<div className="container mx-auto px-4">
<h2 className="text-4xl font-bold tracking-tighter max-w-3xl mx-auto mb-2">{title}</h2>
<div className="text-lg text-muted-foreground max-w-3xl mx-auto whitespace-pre-line">
{paragraphs}
</div>
{after}
</div>
</section>)
}
function getSource({
src,
type
}: {
src: string;
type: 'mp4' | 'webm' | 'ogv'
}) {
const sourceType = `video/${type}`
return [
<source
key={`mobile-${type}`}
media="(max-width: 640px)"
src={src.replace('.mp4', `_mobile.${type}`)}
type={sourceType}
/>,
<source
key={`tablet-${type}`}
media="(max-width: 1024px)"
src={src.replace('.mp4', `_tablet.${type}`)}
type={sourceType}
/>,
<source
key={`hd-${type}`}
media="(max-width: 1920px)"
src={src.replace('.mp4', `_hd.${type}`)}
type={sourceType}
/>,
<source
key={`twok-${type}`}
media="(max-width: 2560px)"
src={src.replace('.mp4', `_2k.${type}`)}
type={sourceType}
/>,
<source
key={`uhd-${type}`}
media="(max-width: 3840px)"
src={src.replace('.mp4', `_uhd.${type}`)}
type={sourceType}
/>,
<source
key={`original-${type}`}
media="(min-width: 3841px)"
src={src.replace('.mp4', `_original.${type}`)}
type={sourceType}
/>,
];
}
function Video({ sourceBase, hidden }: {
sourceBase: string;
hidden: boolean;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const handleScroll = () => {
if (!videoRef.current || hidden) return;
videoRef.current.play();
const scrolled = window.scrollY;
videoRef.current.style.willChange = "transform";
videoRef.current.style.transform = `translateY(${scrolled * 0.5}px)`;
videoRef.current.style.willChange = "unset";
};
const throttledHandleScroll = () => {
requestAnimationFrame(handleScroll);
};
window.addEventListener("scroll", throttledHandleScroll);
handleScroll();
return () => window.removeEventListener("scroll", throttledHandleScroll);
}, [hidden]);
const sources = [...getSource({ src: sourceBase, type: 'mp4' }), ...getSource({ src: sourceBase, type: 'ogv' }), ...getSource({ src: sourceBase, type: 'webm' })]
return (
<video
ref={videoRef}
preload="auto"
autoPlay
muted
loop
playsInline
webkit-playsinline="true"
x5-playsinline="true"
className={cn("w-full h-full object-contain parallax-video", {
hidden,
})}
>
{sources.map(x => x)}
</video>
);
}
const LazyLeafletMap = dynamic(() => import('./event-map.lazy'), {
ssr: false,
loading: () => <Skeleton className="mt-8 h-[368px] w-full" />
})
export default function LandingPage(
{ t }: { t: Translations }
) {
const { theme } = useTheme()
return (
<div>
<Nav t={t} />
<main className="flex flex-col min-h-screen grid-gap-10 gap-10 pb-12">
<section id="hero" className="h-screen relative overflow-hidden dark:bg-black light:bg-white ">
<div className="absolute inset-0 opacity-80">
<Video sourceBase="/videos/ceboola_gradient.mp4" hidden={theme === 'light'} />
<Video sourceBase="/videos/ceboola_gradient-white.mp4" hidden={theme === "dark"} />
</div>
<div className="relative z-10 container mx-auto px-4 h-full flex items-center justify-center">
<div className="text-center font-[JGS7]">
<h1 className="text-5xl sm:text-6xl md:text-8xl font-bold tracking-tighter light:text-background">{t.hero.title}</h1>
<p className="mt-2 text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl-text:7xl 2xl:text-8xl text-primary">{t.hero.subtitle}</p>
<p className="mt-2 text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl-text:7xl 2xl:text-8xl text-primary ">{t.when.date}</p>
</div>
</div>
</section>
<Section id="about" title={t.about.title} paragraphs={<p>{t.about.description}</p>} />
<Section id="where" title={t.where.title} paragraphs={<p>{t.where.location}</p>} after={<LazyLeafletMap t={t} />} />
<Section id="when" title={t.when.title} paragraphs={<>
<p className="text-primary text-3xl font-[JGS7]">{t.when.date}</p>
<p className="mt-4">{t.when.extra}</p></>}
/>
<Section id="tickets" title={t.tickets.title} paragraphs={<p>{t.tickets.status}</p>} />
<Section id="accommodation" title={t.accommodation.title} paragraphs={<p>{t.accommodation.description}</p>} />
<Section id="food" title={t.food.title} paragraphs={<p>{t.food.description}</p>} />
<Section id="contact" title={t.contact.title} paragraphs={<a href={`mailto:${t.contact.email}`}>{t.contact.email}</a>} />
<Section id="credits" title={t.credits.title} paragraphs={<>
<p>{t.credits.usedFonts}</p>
<p><a className="hover:underline" href="https://velvetyne.fr/fonts/jgs-font/">{t.credits.jgs7}</a></p>
<p><a className="hover:underline" href="https://fonts.google.com/specimen/Oxanium">{t.credits.oxanium}</a></p>
</>}
/>
</main>
</div>
)
}

View file

@ -0,0 +1,45 @@
"use client"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import type { Sections, translations } from "@/i18n/translations"
import { cn } from "@/lib/utils"
import { Menu } from "lucide-react"
import { LanguageSelector } from "./ui/language-selector"
export function MobileNav({
t,
linksOrder,
activeSection
}: {
t: typeof translations.pl
linksOrder: Array<Sections>
activeSection: Sections
}) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
<span className="sr-only">{t.mobileNav.toggleMenu}</span>
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[80vw] sm:w-[385px]">
<SheetHeader>
<SheetTitle>{t.mobileNav.menu}</SheetTitle>
</SheetHeader>
<nav className="flex flex-col gap-4 mt-8">
{linksOrder.map((value) => (
<a key={value} href={`#${value}`} className={cn("text-lg hover:text-primary transition-colors", {
'text-primary': activeSection === value
})}>
{t.nav[value]}
</a>
))}
<LanguageSelector />
</nav>
</SheetContent>
</Sheet>
)
}

117
src/components/nav.tsx Normal file
View file

@ -0,0 +1,117 @@
"use client"
import { Button } from "@/components/ui/button"
import { Sections, type translations } from "@/i18n/translations"
import { cn } from "@/lib/utils"
import { MoonIcon, SunIcon } from "lucide-react"
import { useEffect, useState } from "react"
import { MobileNav } from "./mobile-nav"
import { useTheme } from "./providers"
import { LanguageSelector } from "./ui/language-selector"
const linksOrder: Array<keyof (typeof translations.pl)["nav"]> = [
"hero",
"about",
"where",
"when",
"tickets",
"accommodation",
"food",
"contact",
]
export function Nav({
t,
}: {
t: typeof translations.pl
}) {
const { theme, setTheme } = useTheme()
const [activeSection, setActiveSection] = useState<Sections>("about")
useEffect(() => {
const options = {
root: null,
rootMargin: "-10px",
threshold: 0.5, // Adjust the visibility threshold as needed
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const target = entry.target.id as keyof (typeof translations.pl)["nav"]
if (entry.isIntersecting) {
setActiveSection(target);
if (history.replaceState) {
history.replaceState(null, "", `#${target}`);
}
}
});
}, options);
const sections = linksOrder.map(value => document.getElementById(value));
sections.forEach(section => {
if (section) {
observer.observe(section);
}
});
return () => {
sections.forEach(section => {
if (section) {
observer.unobserve(section);
}
});
};
}, []);
return (
<nav className="fixed top-0 left-0 right-0 backdrop-blur-xs bg-background/40 border-b z-[10000]">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex gap-4">
<LanguageSelector />
<a href="#" className="text-xl font-bold tracking-tighter hover:text-primary transition-colors">
<h1>{t.nav.title}</h1>
</a>
</div>
<div className="flex items-center">
{/* Desktop Navigation */}
<div className="hidden md:flex md:items-center md:gap-4 lg:gap-8">
{linksOrder.map((value) => (
<a
key={value}
href={`#${value}`}
className={cn("text-sm md:text-md hover:text-primary transition-colors relative group", {
'text-primary': activeSection === value
})}
>
{t.nav[value]}
<span className={cn("absolute inset-x-0 -bottom-1 h-0.5 bg-primary transform scale-x-0 group-hover:scale-x-100 transition-transform", {
'scale-x-100': activeSection === value
})} />
</a>
))}
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="ml-4"
>
{theme === "dark" ? <SunIcon className="h-5 w-5" /> : <MoonIcon className="h-5 w-5" />}
</Button>
{/* Mobile Navigation */}
<div className="md:hidden ml-2">
<MobileNav t={t} linksOrder={linksOrder} activeSection={activeSection} />
</div>
</div>
</div>
</div>
</nav>
)
}

View file

@ -0,0 +1,43 @@
'use client';
import React, { createContext, useCallback, useContext, useState } from "react";
type Theme = 'dark' | 'light';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>("dark")
const changeTheme = useCallback((theme: Theme) => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
root.classList.add(theme)
}, [])
const updateTheme = useCallback((theme: Theme) => {
setTheme(theme)
changeTheme(theme)
}, [changeTheme, setTheme])
return (
<ThemeContext.Provider value={{ theme, setTheme: updateTheme }}>
{children}
</ThemeContext.Provider>
);
};
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View file

@ -0,0 +1,57 @@
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from '../../lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -0,0 +1,18 @@
'use client'
import { Lang } from "@/i18n/locales";
import Link from "next/link";
import { useParams } from "next/navigation";
export const LanguageSelector = () => {
const params = useParams<{ locale: Lang }>();
const lang = params?.locale || 'pl';
const hash = globalThis?.window?.location?.hash || '';
if (lang === 'pl') return (<>
<Link suppressHydrationWarning className="pt-1" href={`/en${hash}`}>🇬🇧</Link></>);
if (lang === 'en') return (<>
<Link suppressHydrationWarning className="pt-1" href={`/pl${hash}`}>🇵🇱</Link></>);
};

133
src/components/ui/sheet.tsx Normal file
View file

@ -0,0 +1,133 @@
"use client"
import { cn } from "@/lib/utils"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { }
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet, SheetClose,
SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger
}

View file

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

169
src/globals.css Normal file
View file

@ -0,0 +1,169 @@
@import "tailwindcss";
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer utilities {
@font-face {
font-family: "JGS7";
src: url("/fonts/jgs7.woff2") format("woff2"),
url("/fonts/jgs7.woff") format("woff"),
url("/fonts/jgs7.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
html {
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
}
@layer base {
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(0 0% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%);
--primary: hsl(120 100% 40%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(0 0% 96.1%);
--secondary-foreground: hsl(0 0% 9%);
--muted: hsl(0 0% 96.1%);
--muted-foreground: hsl(0 0% 45.1%);
--accent: hsl(0 0% 96.1%);
--accent-foreground: hsl(0 0% 9%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 89.8%);
--input: hsl(0 0% 89.8%);
--ring: hsl(0 0% 3.9%);
--radius: 0.5rem;
}
.dark {
--background: hsl(0 0% 5%);
--foreground: hsl(0 0% 98%);
--card: hsl(0 0% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(0 0% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(120 100% 45%);
--primary-foreground: hsl(0 0% 9%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(0 0% 14.9%);
--muted-foreground: hsl(0 0% 63.9%);
--accent: hsl(0 0% 14.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
html {
scroll-behavior: smooth;
}
}
.parallax-video {
transition: transform 0.25s ease-out;
}
.jgs7 {
font-family: "JGS7" !important;
}
/* Fix for leaflet's weird z-index */
.z-max {
z-index: 10000;
}

10
src/i18n/locales.ts Normal file
View file

@ -0,0 +1,10 @@
// We need this export in here, otherwise lingui.config.ts crashes on macro imports
export const locales = ["en", "pl"] as const;
export type Lang = (typeof locales)[number];
export function getLocale(lang: Lang): Lang {
return locales.includes(lang as Lang) ? lang : "en";
}
export const defaultLocale = locales[0];

143
src/i18n/translations.ts Normal file
View file

@ -0,0 +1,143 @@
const common = {
jgs7: "JGS font Jgs font by Adel Faure. Distributed by velvetyne.fr",
oxanium: "Oxanium",
orgaEmail: "orga@cebula.camp",
};
const pl = {
siteTitle: "CebulaCamp",
nav: {
title: "CEBULACAMP",
hero: "Cebula",
about: "O nas",
when: "Kiedy",
where: "Gdzie",
food: "Wyżywienie",
contact: "Kontakt",
tickets: "Bilety",
accommodation: "Nocleg",
},
mobileNav: {
toggleMenu: "Aktywuj menu",
menu: "Menu",
},
hero: {
title: "CEBULACAMP 2025",
subtitle: "REAKTYWACJA",
},
about: {
title: "O nas",
description:
"Zjazd hakerów, miłośników open source, wolnych duchów. Organizowany przez hakerów dla hakerów. Będzie mate, będzie utopia, będzie chillera.\n\nReaktywacja wydarzenia po długiej przerwie od ostatniej edycji w 2021. Tym razem widzimy się we Wrocławiu, w budynku starej zajezdni tramwajowej który zamienimy na łączone hackcenter i salę prelekcyjną.\n\nSpodziewaj się ciekawych prezentacji, dziwnych instalacji artystycznych i mnóstwa dyskusji. Możesz opowiedzieć o swoim projekcie, zademonstrować skonstruowane zabawki, albo wspólnie coś stworzyć podczas eventu.",
},
where: {
title: "Gdzie",
location:
"Klub „Łącznik”, Tramwajowa 1-3, Wrocław, obok Hackerspace Wrocław",
},
when: {
title: "Kiedy",
date: "28-31.08.2025",
extra:
"Chętnych do pomocy w przygotowaniach zapraszamy już na Day 0, 27 sierpnia",
},
tickets: {
title: "Bilety",
status: "soon",
},
accommodation: {
title: "Nocleg",
description:
"W duchu prawdziwego campu przygotowujemy ogrodzone miejsce do rozbicia namiotu z toaletami i prysznicem. Ilość miejsc namiotowych ograniczona, obowiązuje kolejność rezerwacji. Jest także możliwość zakwaterowania we własnym zakresie w pobliskim hotelu lub akademikach.",
},
food: {
title: "Wyżywienie",
description:
"We własnym zakresie, w okolicy dostępne są knajpy z dowozem, wieczory planujemy umilić wspólnym grillowaniem.",
},
contact: {
title: "Kontakt",
email: common.orgaEmail,
},
credits: {
title: "Uznania",
usedFonts: "Użyte fonty:",
oxanium: common.oxanium,
jgs7: common.jgs7,
},
};
const en = {
siteTitle: "CebulaCamp 2025",
nav: {
title: "CEBULACAMP",
hero: "Onion",
about: "About us",
when: "When",
where: "Where",
food: "Food",
contact: "Contact",
tickets: "Tickets",
accommodation: "Accomodation",
},
mobileNav: {
toggleMenu: "Toggle menu",
menu: "Menu",
},
hero: {
title: "CEBULACAMP 2025",
subtitle: "REACTIVATED",
},
about: {
title: "About us",
description:
"A gathering of hackers, open source enthusiasts, and free spirits. Organized by hackers for hackers. An utopia of mate and chill vibes.\n\nFirst event since a long pause that started on 2021. This year we'll see eachother in Wrocław, in an old tram depot building which we'll turn into a combined hack center and talk stage.\n\nExpect interesting presentations, weird art installations, and lots of discussions. Talk about your project, show off the stuff you've built, or work on something together during the event.",
},
where: {
title: "Where",
location:
"“Łącznik” Club, Tramwajowa 1-3, Wrocław, next to Hackerspace Wrocław",
},
when: {
title: "When",
date: "28-31.08.2025",
extra:
"Those willing to help with preparations are welcome to join on Day 0, August 27",
},
tickets: {
title: "Tickets",
status: "soon",
},
accommodation: {
title: "Accommodation",
description:
"In the true camp spirit, we're preparing a fenced area for pitching tents with toilets and shower facilities. The number of tent spots is limited, first-come-first-served basis. There's also the possibility of arranging your own accommodation in a nearby hotel or student dormitories.",
},
food: {
title: "Food",
description:
"Self-catering, there are restaurants with delivery in the area, and we plan to enhance the evenings with communal barbecues.",
},
contact: {
title: "Contact",
email: common.orgaEmail,
},
credits: {
title: "Credits",
usedFonts: "Used fonts:",
oxanium: common.oxanium,
jgs7: common.jgs7,
},
};
export const translations: {
en: typeof pl;
pl: typeof pl;
} = {
pl: pl,
en: en,
};
export type Translations = typeof pl;
export type Sections = keyof (typeof pl)["nav"];

6
src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

33
src/middleware.ts Normal file
View file

@ -0,0 +1,33 @@
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
const locales = ["en", "pl"];
const defaultLocale = "pl";
function getLocale(request: NextRequest) {
const headers = {
"accept-language": request.headers.get("accept-language") || "",
};
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
}
export const config = {
matcher: "/((?!api|static|.*\\..*|_next).*)",
// matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tailwind.config.ts"],
"exclude": ["node_modules"]
}