Compare commits

..

No commits in common. "main" and "feat/add-new-pages" have entirely different histories.

16 changed files with 194 additions and 389 deletions

17
package-lock.json generated
View file

@ -36,7 +36,6 @@
"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"
@ -9809,22 +9808,6 @@
}
}
},
"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,7 +37,6 @@
"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: 4.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -4,18 +4,15 @@ import dynamic from 'next/dynamic';
import { jgs7, oxanium } from '@/fonts';
import { Lang } from '@/i18n/locales';
import { jgs7 } from '@/fonts';
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";
import { MainpageNav } from './nav';
import { NewsletterPopup } from './newsletter-form';
import { useTheme } from "./providers";
import { Button } from './ui/button';
import { Skeleton } from './ui/skeleton';
import { Lang } from '@/i18n/locales';
function Heading({ children }: { children: ReactElement | string }) {
return <h2 className="text-5xl font-bold tracking-tighter max-w-3xl mx-auto">{children}</h2>
@ -59,6 +56,28 @@ function NewSection({
</section>)
}
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
@ -175,7 +194,7 @@ export default function LandingPage(
return (
<div>
<MainpageNav t={t} currentLocale={currentLocale} />
<MainpageNav 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 ">
@ -188,45 +207,7 @@ 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 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>
<div className='flex flex-col space-y-4 max-w-20 items-center justify-center m-auto'>
<NewsletterPopup t={t} />
</div>
</div>
@ -260,49 +241,24 @@ export default function LandingPage(
<Heading>{t.tickets.title}</Heading>
</div>
<section>
<TextWrapper><p className="text-2xl font-bold text-primary">{t.tickets.status}</p></TextWrapper>
<TextWrapper><p>{t.tickets.status}</p></TextWrapper>
</section>
</>
</NewSection>
<NewSection id="contribute">
<NewSection id="cfp">
<>
<div>
<Heading>{t.contribute.agenda.title}</Heading>
<Heading>{t.cfp.title}</Heading>
</div>
<section>
<TextWrapper>
<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>
<TextWrapper><p>{t.cfp.status}</p></TextWrapper>
</section>
</>
</NewSection>
<NewSection id="details">
<NewSection id="faq">
<>
<div>
<Heading>{t.details.title}</Heading>
@ -313,7 +269,7 @@ export default function LandingPage(
</section>
<section>
<Subheading>{t.faq.accesibility.title}</Subheading>
<TextWrapper><p>{t.faq.accesibility.description} <a href={getEmailUrl()}>{t.contact.email}</a></p></TextWrapper>
<TextWrapper><p>{t.faq.accesibility.description} <a href="mailto:orga@cebula.camp">{t.contact.email}</a></p></TextWrapper>
</section>
<section>
<Subheading>{t.faq.food.title}</Subheading>
@ -329,8 +285,6 @@ 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>
@ -341,13 +295,7 @@ export default function LandingPage(
<Heading>{t.contact.title}</Heading>
</div>
<section>
<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>
<TextWrapper><p><a href={`mailto:${t.contact.email}`}>{t.contact.email}</a></p></TextWrapper>
</section>
</>
</NewSection>
@ -363,6 +311,7 @@ export default function LandingPage(
</>
</NewSection>
</main>
</div >
</div>
)
}

View file

@ -2,40 +2,28 @@
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 ScrollSpy from "react-scrollspy-navigation"
import { useRef } from "react"
import { LanguageSelector } from "./ui/language-selector"
function NavContent({
t,
linksOrder,
externalLinks
linksOrder
}: {
t: typeof translations.pl,
linksOrder: Array<Sections>,
externalLinks: Record<string, string>
linksOrder: Array<Sections>
}) {
const parent = useRef<HTMLDivElement>(null);
useColorSections(parent);
return (
<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"
>
<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>
);
})}
</ScrollSpy>
))}
<LanguageSelector />
</nav>
)
@ -44,11 +32,9 @@ function NavContent({
export function MobileNav({
t,
linksOrder,
externalLinks,
}: {
t: typeof translations.pl
linksOrder: Array<Sections>
externalLinks: Record<string, string>
}) {
return (
<Sheet >
@ -62,7 +48,7 @@ export function MobileNav({
<SheetHeader>
<SheetTitle>{t.mobileNav.menu}</SheetTitle>
</SheetHeader>
<NavContent t={t} linksOrder={linksOrder} externalLinks={externalLinks} />
<NavContent t={t} linksOrder={linksOrder} />
</SheetContent>
</Sheet>
)

View file

@ -14,17 +14,18 @@ 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,62 +1,49 @@
"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 ScrollSpy from 'react-scrollspy-navigation'
import { useRef } from "react"
import { MobileNav } from "./mobile-nav"
import { NavContainer } from "./nav-container"
const linksOrder: Array<keyof (typeof translations.pl)["nav"]> = [
'about',
'tickets',
'contribute',
'cfp',
'details',
'wiki',
'contact'
]
export function MainpageNav({
t,
currentLocale = 'pl' as Lang
}: {
t: typeof translations.pl
currentLocale?: Lang
}) {
const externalLinks: Record<string, string> = {
'wiki': getWikiUrl(currentLocale)
}
const parent = useRef<HTMLDivElement>(null);
useColorSections(parent);
return (
<NavContainer title={t.nav.title}>
<>
<div className="hidden md:flex md:items-center md:gap-4 lg:gap-8">
<ScrollSpy activeClass="nav-active" offsetTop={80}>
<div className="hidden md:flex md:items-center md:gap-4 lg:gap-8" ref={parent}>
{linksOrder.map((value) => {
const isExternal = value in externalLinks;
const href = isExternal ? externalLinks[value] : `#${value}`;
return (
{linksOrder.map((value) => (
<a
key={value}
href={href}
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
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>
);
})}
</ScrollSpy>
))}
</div>
<div className="md:hidden ml-2">
<MobileNav t={t} linksOrder={linksOrder} externalLinks={externalLinks} />
<MobileNav t={t} linksOrder={linksOrder} />
</div>
</>
</NavContainer>
)
}

View file

@ -3,8 +3,6 @@
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 = () => {
@ -17,22 +15,10 @@ export const LanguageSelector = () => {
}
const lang = params?.locale || 'pl';
const otherLang = replacements[lang];
const changedLang = pathname.replace(`/${lang}`, `/${otherLang}`)
const changedLang = pathname.replace(`/${lang}`, `/${replacements[lang]}`)
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>
</>);
if (lang === 'pl') return (<>
<Link suppressHydrationWarning className="pt-1" href={changedLang}>🇬🇧</Link></>);
if (lang === 'en') return (<>
<Link suppressHydrationWarning className="pt-1" href={changedLang}>🇵🇱</Link></>);
};

View file

@ -155,15 +155,3 @@
.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

@ -0,0 +1,75 @@
import { Sections } from "@/i18n/translations";
import { useLayoutEffect, useRef } from "react";
export const linksOrder: Array<Sections> = [
"hero",
"about",
"where",
"when",
"tickets",
"accommodation",
"food",
"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,10 +10,9 @@ const pl = {
title: "CEBULACAMP",
about: "O wydarzeniu",
tickets: "Bilety",
contribute: "Agenda",
cfp: "Zgłoś prelekcję",
details: "FAQ",
contact: "Kontakt",
wiki: "Wiki",
},
mobileNav: {
toggleMenu: "Aktywuj menu",
@ -44,15 +43,11 @@ const pl = {
},
tickets: {
title: "Bilety",
status: `Bilety na Cebula Camp 2025: Reaktywacja zostały wyprzedane!`,
},
contribute: {
title: "Zgłoś się!",
agenda: {
title: "Agenda",
description: "Zobacz pełny harmonogram wydarzeń, prelekcji i warsztatów",
viewButton: "Zobacz agendę",
status: "Wkrótce ™",
},
cfp: {
title: "Zgłoś prelekcję",
status: "Wkrótce ™",
},
faq: {
accommodation: {
@ -77,18 +72,11 @@ const pl = {
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",
@ -118,10 +106,9 @@ const en = {
title: "CEBULACAMP",
about: "About the event",
tickets: "Tickets",
contribute: "Schedule",
cfp: "Call for papers",
details: "FAQ",
contact: "Contact",
wiki: "Wiki",
},
mobileNav: {
toggleMenu: "Toggle menu",
@ -140,7 +127,7 @@ const en = {
title: "FAQ",
where: {
title: "Location",
location: `The event will take place at the Centre for Academic Culture and Local Initiatives Czasoprzestrzeń” on the premises of a former tram depot. The central meeting point will be the Łącznik club, where there will be a place for lectures and communal hacking.\n\nClub "Łącznik", Tramwajowa 1-3, Wrocław, next to Hackerspace Wrocław.`,
location: `The event will take place at the Centre for Academic Culture and Local Initiatives Czasoprzestrzeń on the premises of the former tram depot. The central meeting point will be the Łącznik club, where there will be a place for lectures and joint hacking.\n\nClub "Łącznik", Tramwajowa 1-3, Wrocław, next to Hackerspace Wrocław.`,
},
when: {
title: "When",
@ -151,20 +138,16 @@ const en = {
},
tickets: {
title: "Tickets",
status: `Tickets for Cebula Camp 2025: Reactivated are sold out!`,
},
contribute: {
title: "Contribute!",
agenda: {
title: "Schedule",
description: "View the full schedule of events, talks, and workshops",
viewButton: "View agenda",
status: "Soon ™",
},
cfp: {
title: "Call for papers",
status: "Soon ™",
},
faq: {
accommodation: {
title: "Accommodation",
description: `Participants will have the option of staying overnight in their own tents on the green lawn located on the depot grounds. The entire depot area is fenced, but not guarded. Toilets and showers are available to all participants, and their infrastructure has been adapted to the needs of people with mobility issues.\n\nPeople who prefer other accommodation options can take advantage of the offer of the Zoo Hotel or nearby dormitories.`,
description: `Participants will have the option of staying overnight in their own tents on the green lawn located on the depot grounds. The entire depot area is fenced, but not guarded. Toilets and showers are available to all participants, and their infrastructure has been adapted to the needs of people with limited physical fitness.\n\nPeople who prefer other accommodation options can take advantage of the offer of the Zoo Hotel or nearby dormitories.`,
},
accesibility: {
title: "Accessibility",
@ -173,29 +156,22 @@ const en = {
food: {
title: "Food",
description:
"In the immediate vicinity there is a Biedronka (discount grocery store) and many restaurants offering food delivery, which should provide participants with convenience and access to meals throughout the event. We are also planning a small coffee and tea corner and evenings with a communal barbecue.",
"In the immediate vicinity there is a Biedronka and many restaurants offering food delivery, which will provide participants with convenience and access to meals throughout the event. We are also planning a small coffee and tea corner and evenings with a joint barbecue.",
},
transport: {
title: "Transport and parking",
description:
"As the event is organized at a tram depot, there is a tram and bus stop nearby, going to the city center and the main rail station. There is a parking lot at the venue.",
"As the event is organized at the tram depot, there is a tram and bus stop nearby, going to the city center and the main station. There is a parking lot at the depot.",
},
documents: {
title: "Documents",
rules: "Terms and Conditions",
rules: "Rules of the event",
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",
@ -213,9 +189,9 @@ const en = {
subscribeButton: "Subscribe",
submitSuccess: "Thank you for subscribing!",
submitPending: "Subscribing...",
policyPrivacyCheckboxTitle: "I accept the Privacy Policy",
policyPrivacyCheckboxTitle: "I accept the privacy policy",
policyPrivacyCheckboxDescription:
"I agree to the processing of the provided personal data by “Stowarzyszenie Hackerspace Wrocław for the purpose of receiving information about the event.",
"I agree to the processing of the provided personal data by Hackerspace Wrocław for the purpose of receiving information about the event.",
},
};

View file

@ -1,35 +0,0 @@
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

@ -1,10 +1,8 @@
# Cebula Camp 2025 Privacy Policy
# Privacy Policy for Cebula Camp 2025
(note: this is an informal translation, the legally binding document is its [Polish version](https://cebula.camp/pl/pages/privacy)).
This policy applies to websites operating under the following URLs: [cebula.camp](https://cebula.camp), [cfp.cebula.camp](https://cfp.cebula.camp), [tickets.cebula.camp](https://tickets.cebula.camp), and [news.cebula.camp](https://news.cebula.camp).
This policy applies to our websites (later: Websites) operating under the following URLs: [cebula.camp](https://cebula.camp), [cfp.cebula.camp](https://cfp.cebula.camp), [tickets.cebula.camp](https://tickets.cebula.camp), and [news.cebula.camp](https://news.cebula.camp).
The Administrator and Operator of personal data is “Stowarzyszenie Hackerspace Wrocław” (registered under KRS number: 0000531222) with its registered address at ul. Wróblewskiego 38, 51-627 Wrocław.
The Administrator and Operator of personal data is Stowarzyszenie Hackerspace Wrocław (KRS: 0000531222) with its registered office at ul. Wróblewskiego 38, 51-627 Wrocław.
For inquiries regarding personal data, contact us at: [kontakt@hswro.org](mailto:kontakt@hswro.org).
@ -12,7 +10,7 @@ For inquiries regarding personal data, contact us at: [kontakt@hswro.org](mailto
## User Rights
Providing personal data is voluntary but necessary for providing our service.
Providing personal data is voluntary but necessary for service provision.
By contacting the data administrator, users can request:
@ -31,13 +29,13 @@ Users have the right to file a complaint regarding data processing by the Operat
Websites collect personal data for the following purposes:
- Fulfilling of ordered services,
- Provision of ordered services,
- Newsletter distribution,
- Collecting talk proposals.
Data collection methods include:
- Voluntarily entered data in forms, stored in the Operator's systems,
- Voluntarily entered data in forms, stored in the Operators systems,
- Storing cookies on users' devices.
---
@ -46,13 +44,13 @@ Data collection methods include:
When selling tickets, we collect the following personal data:
1. Buyer's email address (for identification and ticket-related contact),
2. Participant's name and preferred pronouns (if provided, for communication),
3. Buyer's full name, address, and bank account number (for order processing).
1. Purchasers email address (for identification and ticket-related contact),
2. Participants name and preferred pronouns (if provided, for communication),
3. Payers full name, address, and bank account number (for order processing).
Cookies are used to facilitate purchases and maintain cart status. We do not use tracking or marketing cookies.
For payments via bank transfer, our bank and authorized accounting personnel will have access to the sender's transaction details. These details will be processed and stored solely for accounting documentation purposes.
For bank transfer payments, our bank and authorized accounting personnel will have access to the senders transaction details. These details will be processed and stored solely for accounting documentation purposes.
---
@ -60,8 +58,8 @@ For payments via bank transfer, our bank and authorized accounting personnel wil
For newsletter management, we collect:
1. Subscriber's email address,
2. Subscriber's name or nickname (if provided).
1. Subscribers email address,
2. Subscribers name or nickname (if provided).
The email and name (if provided) will be transmitted in an unencrypted form to the subscriber's email server. No personal data is shared with third parties.
@ -73,8 +71,8 @@ Sent emails contain an embedded remote image ('tracking pixel'). This is used to
For collecting talk proposals, we gather:
1. Submitter's email address,
2. Submitter's name or nickname.
1. Submitters email address,
2. Submitters name or nickname.
---
@ -104,6 +102,6 @@ Personal data is processed only as long as necessary for purposes defined by leg
Personal data is not transferred to third countries under data protection regulations, meaning it is not sent outside the European Union.
Login and personal data entry areas are protected with SSL encryption. This ensures that personal data and login credentials entered on the website are encrypted on the user's device and can only be read by the target server.
Login and personal data entry areas are protected with SSL encryption. This ensures that personal data and login credentials entered on the website are encrypted on the users device and can only be read by the target server.
Return to the [homepage](/en/)

View file

@ -1,43 +1,22 @@
Terms and Conditions
# Terms and Conditions of Cebula Camp 2025 event
(note: this is an informal translation, the legally binding document is its [Polish version](https://cebula.camp/pl/pages/rules)).
1. The organizer of the Cebula Camp event (hereinafter referred to as the Event) is “Stowarzyszenie Hackerspace Wrocław” (registered under KRS number 0000531222).
2. The Administrator of the personal data of the Event participants is “Stowarzyszenie Hackerspace Wrocław” (registered under KRS number 0000531222).
3. The Event takes place from August 28 to August 31, 2025, at “Centrum Kultury Akademickiej i Inicjatyw Lokalnych Czasoprzestrzeń”.
# Rules of Cebula Camp 2025 event
1. The organizer of the Cebula Camp event (hereinafter referred to as the Event) is Stowarzyszenie Hackerspace Wrocław (registered under KRS number 0000531222).
2. The administrator of the personal data of the Event participants is Stowarzyszenie Hackerspace Wrocław (registered under KRS number 0000531222).
3. The Event takes place from August 28 to August 31, 2025, at Centrum Kultury Akademickiej i Inicjatyw Lokalnych Czasoprzestrzeń.
4. The Event is a closed and paid event.
5. Each participant of the Event is required to purchase a ticket.
6. The purchase of a ticket entitling participation in the Event constitutes acceptance of these Terms and Conditions.
7. The Event is intended exclusively for adults. The Organizer reserves the right to validate a participant's ID or other document proving their age.
8. Each participant of the Event is required to wear a provided identifier (badge or similar). Only Organizers and Event Staff are authorized to check identifiers. Any loss of an identifier must be immediately reported to the Organizers.
7. The Event is intended exclusively for adults. The Organizer reserves the right to check the participant's age verification document.
8. Each participant of the Event is required to wear the assigned ID badge. Only Organizers and Event Staff are authorized to check badges. Any loss of an ID badge must be immediately reported to the Organizers.
9. The Organizers are not responsible for lost or stolen items. Items found during the Event (including documents) will be available for retrieval from the Organizers.
10. Bringing animals to the Event is prohibited, except for assistance dogs.
11. It is forbidden to take photos, record videos, or stream live video at the Event without the prior explicit consent of all persons appearing in the frame.
12. The Organizer reserves the right to record, stream, and photograph presenters after obtaining their written consent.
13. It is prohibited to be significantly under the influence of psychoactive substances while at the Event.
14. The Organizer reserves the right to remove any individuals from the Event premises who violate the Terms and Conditions, disrupt order, or pose a threat.
13. It is prohibited to be significantly intoxicated by psychoactive substances while at the Event.
14. The Organizer reserves the right to remove from the Event premises any individuals who violate the Terms and Conditions, disrupt order, or pose a threat.
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/en/pages/rules.
16. The Terms and Conditions of the Event are available at [https://cebula.camp/regulamin](https://cebula.camp/regulamin).
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.
18. In disputed or unspecified matters, the final decision rests with the Organizers, in accordance with Polish law.
Return to the [homepage](/en/)

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/pl/pages/rules.
16. Regulamin Wydarzenia jest dostępny na stronie https://cebula.camp/regulamin.
17. Organizator zastrzega sobie prawo do zmiany Regulaminu. Zmiany w regulaminie będą komunikowane drogą mailową oraz dostępne na stronie.