Compare commits

...

19 commits

Author SHA1 Message Date
Dariusz Niemczyk cd3e323464
fix: eslint
All checks were successful
/ deploy (push) Successful in 1s
2025-08-28 18:35:52 +02:00
Dariusz Niemczyk 7bceaab551
refactor: centralize external links configuration
All checks were successful
/ deploy (push) Successful in 1s
- Create external-links.ts with all external URLs in one place
- Add locale-aware functions for wiki and CFP links
- Update nav.tsx and landing-page.tsx to use centralized config
- Remove duplicate URL definitions across components
2025-08-27 18:22:00 +02:00
Dariusz Niemczyk 8a629c3de8
feat: simplify navigation and add wiki link
All checks were successful
/ deploy (push) Successful in 3s
- Remove standalone agenda page (deleted agenda/page.tsx)
- Remove CFP/art submission sections from contribute area
- Add direct wiki link to navigation and FAQ documents
- Add wiki button alongside schedule button in hero section
- Update navigation to include wiki as external link
- Clean up translations by removing unused CFP/art text
2025-08-27 14:09:04 +02:00
Mewp 10bfa5e561 Fix schedule link 2025-08-21 16:11:49 +02:00
Dariusz Niemczyk 5127ee7967
fix: move hardcoded translation in landing-page to translations 2025-08-05 22:31:10 +02:00
Dariusz Niemczyk e69926a162
feat: add agenda page and update navigation
- Create dedicated agenda page with full-sized pretalx widget
- Update navigation: "Zgłoś się\!/CFP" → "Agenda/Schedule"
- Move agenda preview to top of contribute section
- Add calendar icon to agenda button in hero section
- Reorder hero buttons: agenda first, newsletter second
- Force light theme on agenda page for better widget compatibility
2025-08-05 20:55:56 +02:00
Dariusz Niemczyk f1ed363830
feat: new tickets texts
Some checks failed
/ deploy (push) Failing after 54s
2025-06-12 14:54:43 +02:00
Dariusz Niemczyk 78db02a03d
feat: modify the cfp/art texts
Some checks failed
/ deploy (push) Failing after 54s
2025-05-12 13:51:58 +02:00
Dariusz Niemczyk 956cd060f0
feat: add new texts
Some checks failed
/ deploy (push) Failing after 57s
2025-05-11 15:18:22 +02:00
palid 38d94270a3 Update src/pages/pl/rules.mdx
All checks were successful
/ deploy (push) Successful in 1s
2025-04-22 19:36:50 +00:00
palid e9c5416f89 Update src/pages/en/rules.mdx
Some checks failed
/ deploy (push) Has been cancelled
2025-04-22 19:36:35 +00:00
Dariusz Niemczyk 25a98cbb85
feat: add some longer contact page content
Some checks failed
/ deploy (push) Failing after 54s
2025-04-14 13:28:13 +02:00
Dariusz Niemczyk c1ead36042
fix: scrollspy offset
Some checks failed
/ deploy (push) Failing after 54s
2025-04-13 22:02:21 +02:00
Dariusz Niemczyk 65a8ea8ab8 fix: properly fix colors-sections
Some checks failed
/ deploy (push) Failing after 56s
2025-04-13 19:41:36 +00:00
q3k 29c1252633 landing: do not mention how many tickets there are per space
Some checks failed
/ deploy (push) Failing after 54s
This seems like an internal detail, and is probably likely to change
(ie. we will issue more vouchers as they legitimately run out and if it
won't starve other spaces).
2025-04-13 19:46:57 +02:00
Dariusz Niemczyk 6da5da5bee
fox: add header margin to scroll
Some checks failed
/ deploy (push) Failing after 53s
2025-04-13 19:01:03 +02:00
Dariusz Niemczyk 103e343f22 fix: links
Some checks failed
/ deploy (push) Failing after 53s
2025-04-13 16:58:59 +00:00
q3k 2c0d70975a look: new favicon
Some checks failed
/ deploy (push) Failing after 53s
The wireframe render is barely visible on a light background. This turns
it into a silhouette (preserving our colour scheme) and rotates it by
~45 degrees to make it fill out the frame more.
2025-04-13 18:13:44 +02:00
q3k af1a4e6c74 look: move language selector to RHS, don't use country flags
Some checks failed
/ deploy (push) Failing after 1s
The language selection being a flag never sat right wight me:

 1. Its positioning next to 'CEBULACAMP' implied more that it's a
    cebula.camp logo or intrinsic flag of the event or something.
 2. I think it's the first site I've ever seen that has a language
    selector on the left hand side of the navigation bar.
 3. Using country flags as language icons is wrong [1].

[1] - just ask the Swiss or the Belgians.
2025-04-13 17:03:40 +02:00
15 changed files with 338 additions and 158 deletions

17
package-lock.json generated
View file

@ -36,6 +36,7 @@
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-leaflet": "^5.0.0-rc.2",
"react-scrollspy-navigation": "^2.0.6",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
@ -9808,6 +9809,22 @@
}
}
},
"node_modules/react-scrollspy-navigation": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/react-scrollspy-navigation/-/react-scrollspy-navigation-2.0.6.tgz",
"integrity": "sha512-BBnIEI9BsCPIMnp3z/OIEJ6mRSlDgGYf8wty5IjR8nOIhIeRhQp6eDUAKFMyHRoM2RUInA2NDu5DutIFTphoKQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/toviszsolt"
},
{
"type": "paypal",
"url": "https://www.paypal.com/paypalme/toviszsolt"
}
],
"license": "MIT"
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",

View file

@ -37,6 +37,7 @@
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-leaflet": "^5.0.0-rc.2",
"react-scrollspy-navigation": "^2.0.6",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -7,6 +7,7 @@ import dynamic from 'next/dynamic';
import { jgs7, oxanium } from '@/fonts';
import { Lang } from '@/i18n/locales';
import { Translations } from "@/i18n/translations";
import { getWikiUrl, getCfpScheduleUrl, getEmailUrl } from '@/lib/external-links';
import { cn } from "@/lib/utils";
import Link from 'next/link';
import { ReactElement, useEffect, useRef } from "react";
@ -174,7 +175,7 @@ export default function LandingPage(
return (
<div>
<MainpageNav t={t} />
<MainpageNav t={t} currentLocale={currentLocale} />
<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 ">
@ -187,7 +188,45 @@ export default function LandingPage(
<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.details.when.date}</p>
<div className='flex flex-col space-y-4 max-w-20 items-center justify-center m-auto'>
<div className='flex flex-col space-y-4 items-center justify-center m-auto'>
<div className='flex flex-row gap-4'>
<Button className={`${oxanium.className} text-xl uppercase cursor-pointer`}>
<Link href={getCfpScheduleUrl(currentLocale)} className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{t.contribute.agenda.title}
</Link>
</Button>
<Button className={`${oxanium.className} text-xl uppercase cursor-pointer`}>
<Link href={getWikiUrl(currentLocale)} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
{t.nav.wiki}
</Link>
</Button>
</div>
<NewsletterPopup t={t} />
</div>
</div>
@ -221,29 +260,49 @@ export default function LandingPage(
<Heading>{t.tickets.title}</Heading>
</div>
<section>
<TextWrapper><p>{t.tickets.status}</p></TextWrapper>
<TextWrapper><p className="text-2xl font-bold text-primary">{t.tickets.status}</p></TextWrapper>
</section>
</>
</NewSection>
<NewSection id="contribute">
<>
<div>
<Heading>{t.contribute.agenda.title}</Heading>
</div>
<section>
<TextWrapper>
<Button className={`${oxanium.className} text-xl mt-4 uppercase cursor-pointer`}>
<Link href="https://tickets.cebula.camp/cebulacamp/2025/">{t.tickets.link}</Link>
</Button>
<div className="border border-border rounded-md bg-muted/30 p-8 text-center">
<div className="mb-6">
<svg
className="w-16 h-16 mx-auto text-primary mb-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<p className="text-lg text-muted-foreground mb-4">
{t.contribute.agenda.description}
</p>
</div>
<Button className={`${oxanium.className} text-xl uppercase cursor-pointer`} size="lg">
<Link href={getCfpScheduleUrl(currentLocale)}>
{t.contribute.agenda.viewButton}
</Link>
</Button>
</div>
</TextWrapper>
</section>
</>
</NewSection>
<NewSection id="cfp">
<>
<div>
<Heading>{t.cfp.title}</Heading>
</div>
<section>
<TextWrapper><p>{t.cfp.status}</p></TextWrapper>
</section>
</>
</NewSection>
<NewSection id="faq">
<NewSection id="details">
<>
<div>
<Heading>{t.details.title}</Heading>
@ -254,7 +313,7 @@ export default function LandingPage(
</section>
<section>
<Subheading>{t.faq.accesibility.title}</Subheading>
<TextWrapper><p>{t.faq.accesibility.description} <a href="mailto:orga@cebula.camp">{t.contact.email}</a></p></TextWrapper>
<TextWrapper><p>{t.faq.accesibility.description} <a href={getEmailUrl()}>{t.contact.email}</a></p></TextWrapper>
</section>
<section>
<Subheading>{t.faq.food.title}</Subheading>
@ -270,6 +329,8 @@ export default function LandingPage(
<a className='hover:text-primary' href={`/${currentLocale}/pages/rules`}>📜 {t.faq.documents.rules}</a></p></TextWrapper>
<TextWrapper><p>
<a className='hover:text-primary' href={`/${currentLocale}/pages/privacy`}>📜 {t.faq.documents.privacyPolicy}</a></p></TextWrapper>
<TextWrapper><p>
<a className='hover:text-primary' href={getWikiUrl(currentLocale)} target="_blank" rel="noopener noreferrer">📚 {t.faq.documents.wiki}</a></p></TextWrapper>
</section>
</>
</NewSection>
@ -280,7 +341,13 @@ export default function LandingPage(
<Heading>{t.contact.title}</Heading>
</div>
<section>
<TextWrapper><p><a href={`mailto:${t.contact.email}`}>{t.contact.email}</a></p></TextWrapper>
<TextWrapper><p>{t.contact.details.line1}</p></TextWrapper>
<br />
<TextWrapper><p>📬 <a href={getEmailUrl()}>{t.contact.email}</a> {t.contact.details.line2}</p></TextWrapper>
<br />
<TextWrapper><p>{t.contact.details.line3}</p></TextWrapper>
<br />
<TextWrapper><p>{t.contact.details.line4}</p></TextWrapper>
</section>
</>
</NewSection>
@ -296,7 +363,6 @@ export default function LandingPage(
</>
</NewSection>
</main>
</div>
</div >
)
}

View file

@ -2,28 +2,40 @@
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { useColorSections } from "@/hooks/color-sections"
import type { Sections, translations } from "@/i18n/translations"
import { Menu } from "lucide-react"
import { useRef } from "react"
import ScrollSpy from "react-scrollspy-navigation"
import { LanguageSelector } from "./ui/language-selector"
function NavContent({
t,
linksOrder
linksOrder,
externalLinks
}: {
t: typeof translations.pl,
linksOrder: Array<Sections>
linksOrder: Array<Sections>,
externalLinks: Record<string, string>
}) {
const parent = useRef<HTMLDivElement>(null);
useColorSections(parent);
return (
<nav className="flex flex-col gap-4 mt-8" ref={parent}>
{linksOrder.map((value) => (
<a key={value} href={`#${value}`} className="text-lg hover:text-primary transition-colors">
{t.nav[value]}
</a>
))}
<nav className="flex flex-col gap-4 mt-8">
<ScrollSpy activeClass="nav-active" offsetTop={80}>
{linksOrder.map((value) => {
const isExternal = value in externalLinks;
const href = isExternal ? externalLinks[value] : `#${value}`;
return (
<a
key={value}
href={href}
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className="text-lg hover:text-primary transition-colors"
>
{t.nav[value]}
</a>
);
})}
</ScrollSpy>
<LanguageSelector />
</nav>
)
@ -32,9 +44,11 @@ function NavContent({
export function MobileNav({
t,
linksOrder,
externalLinks,
}: {
t: typeof translations.pl
linksOrder: Array<Sections>
externalLinks: Record<string, string>
}) {
return (
<Sheet >
@ -48,7 +62,7 @@ export function MobileNav({
<SheetHeader>
<SheetTitle>{t.mobileNav.menu}</SheetTitle>
</SheetHeader>
<NavContent t={t} linksOrder={linksOrder} />
<NavContent t={t} linksOrder={linksOrder} externalLinks={externalLinks} />
</SheetContent>
</Sheet>
)

View file

@ -14,18 +14,17 @@ export function NavContainer({ children, title, }: { children: React.ReactNode,
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex gap-4">
<LanguageSelector />
<Link href="/" className="text-xl font-bold tracking-tighter hover:text-primary transition-colors">
<h1>{title}</h1>
</Link>
</div>
<div className="flex items-center">
{children}
<LanguageSelector />
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="ml-4"
>
{theme === "dark" ? (
<SunIcon className="h-5 w-5" />

View file

@ -1,49 +1,62 @@
"use client"
import { useColorSections } from "@/hooks/color-sections"
import { type translations } from "@/i18n/translations"
import { Lang } from '@/i18n/locales'
import { getWikiUrl } from '@/lib/external-links'
import { cn } from "@/lib/utils"
import { useRef } from "react"
import ScrollSpy from 'react-scrollspy-navigation'
import { MobileNav } from "./mobile-nav"
import { NavContainer } from "./nav-container"
const linksOrder: Array<keyof (typeof translations.pl)["nav"]> = [
'about',
'tickets',
'cfp',
'contribute',
'details',
'wiki',
'contact'
]
export function MainpageNav({
t,
currentLocale = 'pl' as Lang
}: {
t: typeof translations.pl
currentLocale?: Lang
}) {
const parent = useRef<HTMLDivElement>(null);
useColorSections(parent);
const externalLinks: Record<string, string> = {
'wiki': getWikiUrl(currentLocale)
}
return (
<NavContainer title={t.nav.title}>
<>
<div className="hidden md:flex md:items-center md:gap-4 lg:gap-8" ref={parent}>
<div className="hidden md:flex md:items-center md:gap-4 lg:gap-8">
<ScrollSpy activeClass="nav-active" offsetTop={80}>
{linksOrder.map((value) => (
<a
key={value}
href={`#${value}`}
className="text-sm md:text-md hover:text-primary transition-colors relative group will-change-[color]"
>
{t.nav[value]}
<span data-sub={value} className={cn("absolute inset-x-0 -bottom-1 h-0.5 bg-primary transform scale-x-0 group-hover:scale-x-100 transition-transform will-change-transform", {
})} />
</a>
))}
{linksOrder.map((value) => {
const isExternal = value in externalLinks;
const href = isExternal ? externalLinks[value] : `#${value}`;
return (
<a
key={value}
href={href}
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className="text-sm md:text-md hover:text-primary transition-colors relative group will-change-[color]"
>
{t.nav[value]}
<span data-sub={value} className={cn("absolute inset-x-0 -bottom-1 h-0.5 bg-primary transform scale-x-0 group-hover:scale-x-100 transition-transform will-change-transform", {
})} />
</a>
);
})}
</ScrollSpy>
</div>
<div className="md:hidden ml-2">
<MobileNav t={t} linksOrder={linksOrder} />
<MobileNav t={t} linksOrder={linksOrder} externalLinks={externalLinks} />
</div>
</>
</NavContainer>
)
}

View file

@ -3,6 +3,8 @@
import { Lang } from "@/i18n/locales";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { Button } from "./button";
import { LanguagesIcon } from "lucide-react";
export const LanguageSelector = () => {
@ -15,10 +17,22 @@ export const LanguageSelector = () => {
}
const lang = params?.locale || 'pl';
const changedLang = pathname.replace(`/${lang}`, `/${replacements[lang]}`)
const otherLang = replacements[lang];
const changedLang = pathname.replace(`/${lang}`, `/${otherLang}`)
if (lang === 'pl') return (<>
<Link suppressHydrationWarning className="pt-1" href={changedLang}>🇬🇧</Link></>);
if (lang === 'en') return (<>
<Link suppressHydrationWarning className="pt-1" href={changedLang}>🇵🇱</Link></>);
return (<>
<Button
variant="ghost"
size="icon"
className="md:ml-6 w-15"
>
<Link
href={changedLang}
className="flex space-x-2 items-center"
>
<LanguagesIcon className="h-5 w-5 mr-1" />
<span>{otherLang.toUpperCase()}</span>
</Link>
</Button>
</>);
};

View file

@ -155,3 +155,15 @@
.z-max {
z-index: 10000;
}
section {
scroll-margin-top: calc(var(--spacing) * 16 + var(--spacing) * 4);
}
.nav-active {
color: var(--color-primary) !important;
& span {
--tw-scale-x: 100%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}

View file

@ -1,73 +0,0 @@
import { Sections } from "@/i18n/translations";
import { useLayoutEffect, useRef } from "react";
export const linksOrder: Array<Sections> = [
"details",
"title",
"about",
"tickets",
"cfp",
"contact"
]
export function useColorSections(parent: React.RefObject<HTMLDivElement | null>) {
const previous = useRef<Sections>(linksOrder[0])
useLayoutEffect(() => {
if (parent.current === null) return;
const options = {
root: null,
rootMargin: "-10px",
threshold: 0.5, // Adjust the visibility threshold as needed
};
const sections = linksOrder.map(value => document.getElementById(value));
const subs = linksOrder.reduce((acc, value) => {
acc[value] = parent.current!.querySelector('[data-sub="' + value + '"]')!;
return acc;
}, {} as Record<Sections, HTMLAnchorElement>);
const links = linksOrder.reduce((acc, value) => {
acc[value] = parent.current!.querySelector('[href="#' + value + '"]')!;
return acc;
}, {} as Record<Sections, HTMLAnchorElement>);
console.log(links)
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const target = entry.target.id as Sections
if (entry.isIntersecting) {
// FIXME: This seems to be VERY broken on firefox.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1250972
// It basically spikes up CPU usage to some enormous values just to update the hash, like WTF firefox.
// if (history.replaceState) {
// timeout = setTimeout(() => {
// history.replaceState(null, "", `#${target}`)
// }, 150)
// }
subs[previous.current]?.classList.remove('scale-x-100');
links[previous.current]?.classList.remove('text-primary');
previous.current = target;
subs[previous.current]?.classList.add('scale-x-100');
links[previous.current]?.classList.add('text-primary');
break;
}
}
}, options);
sections.forEach(section => {
if (section) {
observer.observe(section);
}
});
return () => {
observer.disconnect()
};
}, [parent]);
}

View file

@ -10,9 +10,10 @@ const pl = {
title: "CEBULACAMP",
about: "O wydarzeniu",
tickets: "Bilety",
cfp: "Zgłoś prelekcję",
contribute: "Agenda",
details: "FAQ",
contact: "Kontakt",
wiki: "Wiki",
},
mobileNav: {
toggleMenu: "Aktywuj menu",
@ -43,15 +44,15 @@ const pl = {
},
tickets: {
title: "Bilety",
status: `Rusza sprzedaż pierwszej puli biletów na Cebula Camp 2025: Reaktywacja!
Od 20 kwietnia 2025 r. będzie można kupić bilet. Każdy z sześciu polskich Hackerspaceów ma voucher, który pozwala mu kupić 10 biletów. Skontaktuj się więc z najbliższym HS i zarezerwuj swój bilet jak najszybciej!
W miejscu, gdzie będzie nasz Camp, mamy do dyspozycji przestrzeń, którą wykorzystamy jako małe pole namiotowe. Jeżeli chcesz nocować w swoim namiocie, przy zakupie biletu dodaj nocleg na polu namiotowym jako dodatek do biletu. Nie pobieramy za to żadnych dodatkowych opłat, ale liczba miejsc jest ograniczona, a pula wspólna dla wszystkich.`,
link: "Kup bilet tutaj",
status: `Bilety na Cebula Camp 2025: Reaktywacja zostały wyprzedane!`,
},
cfp: {
title: "Zgłoś prelekcję",
status: "Wkrótce ™",
contribute: {
title: "Zgłoś się!",
agenda: {
title: "Agenda",
description: "Zobacz pełny harmonogram wydarzeń, prelekcji i warsztatów",
viewButton: "Zobacz agendę",
},
},
faq: {
accommodation: {
@ -76,11 +77,18 @@ W miejscu, gdzie będzie nasz Camp, mamy do dyspozycji przestrzeń, którą wyko
title: "Dokumenty",
rules: "Regulamin wydarzenia",
privacyPolicy: "Polityka prywatności",
wiki: "Wiki CebulaCamp",
},
},
contact: {
title: "Kontakt",
email: common.orgaEmail,
details: {
line1: `Masz pytania, pomysły, albo po prostu chcesz się przywitać?`,
line2: `— napisz śmiało.`,
line3: `Szukasz sposobu, żeby się zaangażować, poprowadzić warsztat, zasponsorować coś szalonego albo po prostu poczuć klimat? Odezwij się! Jesteśmy małą, przyjazną ekipą i czytamy każdą wiadomość (tak, nawet te dziwne).`,
line4: `Widzimy się we Wrocławiu 🧅`,
},
},
credits: {
title: "Uznania",
@ -110,9 +118,10 @@ const en = {
title: "CEBULACAMP",
about: "About the event",
tickets: "Tickets",
cfp: "Call for papers",
contribute: "Schedule",
details: "FAQ",
contact: "Contact",
wiki: "Wiki",
},
mobileNav: {
toggleMenu: "Toggle menu",
@ -142,16 +151,15 @@ const en = {
},
tickets: {
title: "Tickets",
status: `The sale of the first batch of tickets for Cebula Camp 2025: Reactivation is starting!
You will be able to buy a ticket from April 20, 2025. Each of the six Polish Hackerspaces has a voucher that allows them to buy 10 tickets. Contact your nearest hackerspace and book your ticket as soon as possible!
We have a small camping ground next to the venue for our use. If you want to bring a tent and stay in it overnight, when buying a ticket, add "overnight stay at the camping site" as an addition to the ticket. We do not charge any additional fees for this, but the number of places is limited and the pool is shared by everyone.`,
link: "Get your ticket here!",
status: `Tickets for Cebula Camp 2025: Reactivated are sold out!`,
},
cfp: {
title: "Call for papers",
status: "Soon ™",
contribute: {
title: "Contribute!",
agenda: {
title: "Schedule",
description: "View the full schedule of events, talks, and workshops",
viewButton: "View agenda",
},
},
faq: {
accommodation: {
@ -176,11 +184,18 @@ We have a small camping ground next to the venue for our use. If you want to bri
title: "Documents",
rules: "Terms and Conditions",
privacyPolicy: "Privacy Policy",
wiki: "CebulaCamp Wiki",
},
},
contact: {
title: "Contact",
email: common.orgaEmail,
details: {
line1: `Got questions, cool ideas, or just want to say hi?`,
line2: `— we're listening.`,
line3: `Whether youre looking to help out, run a workshop, sponsor something wild, or just want to vibe with the crew—drop us a line. We're a small, friendly team and we read every message (yes, even the weird ones).`,
line4: `See you in Wrocław 🧅`,
},
},
credits: {
title: "Credits",

35
src/lib/external-links.ts Normal file
View file

@ -0,0 +1,35 @@
import { Lang } from "@/i18n/locales";
export const EXTERNAL_URLS = {
email: "orga@cebula.camp",
} as const;
export function getWikiUrl(locale: Lang): string {
return `https://wiki.cebula.camp/${locale}/home`;
}
export function getCfpScheduleUrl(locale: Lang): string {
return `https://cfp.cebula.camp/camp-2025/locale/set?locale=${locale}&next=/camp-2025/schedule/`;
}
export function getCfpUrl(): string {
return `https://cfp.cebula.camp/camp-2025/cfp`;
}
export function getCfpArtUrl(): string {
return `https://cfp.cebula.camp/camp-2025-art/cfp`;
}
export function getEmailUrl(): string {
return `mailto:${EXTERNAL_URLS.email}`;
}
export function getExternalLinks(locale: Lang) {
return {
wiki: getWikiUrl(locale),
cfpSchedule: getCfpScheduleUrl(locale),
cfp: getCfpUrl(),
cfpArt: getCfpArtUrl(),
email: getEmailUrl(),
};
}

View file

@ -34,7 +34,7 @@ Terms and Conditions
15. The Organizer is not responsible for and does not endorse any opinions expressed by participants during the Event.
16. The Terms and Conditions of the Event are available at [https://cebula.camp/regulamin](https://cebula.camp/regulamin).
16. The Terms and Conditions of the Event are available at https://cebula.camp/en/pages/rules.
17. The Organizer reserves the right to amend these Terms and Conditions. Any changes will be communicated via email and made available on the website.

View file

@ -32,7 +32,7 @@ Regulamin
15. Organizator nie ponosi odpowiedzialności i nie utożsamia się z opiniami wygłaszanymi przez Uczestników podczas trwania Wydarzenia.
16. Regulamin Wydarzenia jest dostępny na stronie https://cebula.camp/regulamin.
16. Regulamin Wydarzenia jest dostępny na stronie https://cebula.camp/pl/pages/rules.
17. Organizator zastrzega sobie prawo do zmiany Regulaminu. Zmiany w regulaminie będą komunikowane drogą mailową oraz dostępne na stronie.