Compare commits
14 commits
q3k/no-tic
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3e323464 | ||
|
|
7bceaab551 | ||
|
|
8a629c3de8 | ||
|
|
10bfa5e561 | ||
|
|
5127ee7967 | ||
|
|
e69926a162 | ||
|
|
f1ed363830 | ||
|
|
78db02a03d | ||
|
|
956cd060f0 | ||
|
|
38d94270a3 | ||
|
|
e9c5416f89 | ||
|
|
25a98cbb85 | ||
|
|
c1ead36042 | ||
|
|
65a8ea8ab8 |
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -36,6 +36,7 @@
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-leaflet": "^5.0.0-rc.2",
|
"react-leaflet": "^5.0.0-rc.2",
|
||||||
|
"react-scrollspy-navigation": "^2.0.6",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.24.1"
|
"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": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-leaflet": "^5.0.0-rc.2",
|
"react-leaflet": "^5.0.0-rc.2",
|
||||||
|
"react-scrollspy-navigation": "^2.0.6",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import dynamic from 'next/dynamic';
|
||||||
import { jgs7, oxanium } from '@/fonts';
|
import { jgs7, oxanium } from '@/fonts';
|
||||||
import { Lang } from '@/i18n/locales';
|
import { Lang } from '@/i18n/locales';
|
||||||
import { Translations } from "@/i18n/translations";
|
import { Translations } from "@/i18n/translations";
|
||||||
|
import { getWikiUrl, getCfpScheduleUrl, getEmailUrl } from '@/lib/external-links';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ReactElement, useEffect, useRef } from "react";
|
import { ReactElement, useEffect, useRef } from "react";
|
||||||
|
|
@ -174,7 +175,7 @@ export default function LandingPage(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MainpageNav t={t} />
|
<MainpageNav t={t} currentLocale={currentLocale} />
|
||||||
<main className="flex flex-col min-h-screen grid-gap-10 gap-10 pb-12">
|
<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 ">
|
<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>
|
<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.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>
|
<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} />
|
<NewsletterPopup t={t} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -221,24 +260,44 @@ export default function LandingPage(
|
||||||
<Heading>{t.tickets.title}</Heading>
|
<Heading>{t.tickets.title}</Heading>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<TextWrapper><p>{t.tickets.status}</p></TextWrapper>
|
<TextWrapper><p className="text-2xl font-bold text-primary">{t.tickets.status}</p></TextWrapper>
|
||||||
<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>
|
|
||||||
</TextWrapper>
|
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
</NewSection>
|
</NewSection>
|
||||||
|
|
||||||
|
<NewSection id="contribute">
|
||||||
<NewSection id="cfp">
|
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Heading>{t.cfp.title}</Heading>
|
<Heading>{t.contribute.agenda.title}</Heading>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<TextWrapper><p>{t.cfp.status}</p></TextWrapper>
|
<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>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
</NewSection>
|
</NewSection>
|
||||||
|
|
@ -254,7 +313,7 @@ export default function LandingPage(
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<Subheading>{t.faq.accesibility.title}</Subheading>
|
<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>
|
||||||
<section>
|
<section>
|
||||||
<Subheading>{t.faq.food.title}</Subheading>
|
<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>
|
<a className='hover:text-primary' href={`/${currentLocale}/pages/rules`}>📜 {t.faq.documents.rules}</a></p></TextWrapper>
|
||||||
<TextWrapper><p>
|
<TextWrapper><p>
|
||||||
<a className='hover:text-primary' href={`/${currentLocale}/pages/privacy`}>📜 {t.faq.documents.privacyPolicy}</a></p></TextWrapper>
|
<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>
|
</section>
|
||||||
</>
|
</>
|
||||||
</NewSection>
|
</NewSection>
|
||||||
|
|
@ -280,7 +341,13 @@ export default function LandingPage(
|
||||||
<Heading>{t.contact.title}</Heading>
|
<Heading>{t.contact.title}</Heading>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<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>
|
</section>
|
||||||
</>
|
</>
|
||||||
</NewSection>
|
</NewSection>
|
||||||
|
|
@ -296,7 +363,6 @@ export default function LandingPage(
|
||||||
</>
|
</>
|
||||||
</NewSection>
|
</NewSection>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,40 @@
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
||||||
import { useColorSections } from "@/hooks/color-sections"
|
|
||||||
import type { Sections, translations } from "@/i18n/translations"
|
import type { Sections, translations } from "@/i18n/translations"
|
||||||
import { Menu } from "lucide-react"
|
import { Menu } from "lucide-react"
|
||||||
import { useRef } from "react"
|
import ScrollSpy from "react-scrollspy-navigation"
|
||||||
import { LanguageSelector } from "./ui/language-selector"
|
import { LanguageSelector } from "./ui/language-selector"
|
||||||
|
|
||||||
function NavContent({
|
function NavContent({
|
||||||
t,
|
t,
|
||||||
linksOrder
|
linksOrder,
|
||||||
|
externalLinks
|
||||||
}: {
|
}: {
|
||||||
t: typeof translations.pl,
|
t: typeof translations.pl,
|
||||||
linksOrder: Array<Sections>
|
linksOrder: Array<Sections>,
|
||||||
|
externalLinks: Record<string, string>
|
||||||
}) {
|
}) {
|
||||||
const parent = useRef<HTMLDivElement>(null);
|
|
||||||
useColorSections(parent);
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex flex-col gap-4 mt-8" ref={parent}>
|
<nav className="flex flex-col gap-4 mt-8">
|
||||||
{linksOrder.map((value) => (
|
<ScrollSpy activeClass="nav-active" offsetTop={80}>
|
||||||
<a key={value} href={`#${value}`} className="text-lg hover:text-primary transition-colors">
|
|
||||||
{t.nav[value]}
|
{linksOrder.map((value) => {
|
||||||
</a>
|
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 />
|
<LanguageSelector />
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|
@ -32,9 +44,11 @@ function NavContent({
|
||||||
export function MobileNav({
|
export function MobileNav({
|
||||||
t,
|
t,
|
||||||
linksOrder,
|
linksOrder,
|
||||||
|
externalLinks,
|
||||||
}: {
|
}: {
|
||||||
t: typeof translations.pl
|
t: typeof translations.pl
|
||||||
linksOrder: Array<Sections>
|
linksOrder: Array<Sections>
|
||||||
|
externalLinks: Record<string, string>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Sheet >
|
<Sheet >
|
||||||
|
|
@ -48,7 +62,7 @@ export function MobileNav({
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{t.mobileNav.menu}</SheetTitle>
|
<SheetTitle>{t.mobileNav.menu}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<NavContent t={t} linksOrder={linksOrder} />
|
<NavContent t={t} linksOrder={linksOrder} externalLinks={externalLinks} />
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,62 @@
|
||||||
"use client"
|
"use client"
|
||||||
import { useColorSections } from "@/hooks/color-sections"
|
|
||||||
import { type translations } from "@/i18n/translations"
|
import { type translations } from "@/i18n/translations"
|
||||||
|
import { Lang } from '@/i18n/locales'
|
||||||
|
import { getWikiUrl } from '@/lib/external-links'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useRef } from "react"
|
import ScrollSpy from 'react-scrollspy-navigation'
|
||||||
import { MobileNav } from "./mobile-nav"
|
import { MobileNav } from "./mobile-nav"
|
||||||
import { NavContainer } from "./nav-container"
|
import { NavContainer } from "./nav-container"
|
||||||
|
|
||||||
|
|
||||||
const linksOrder: Array<keyof (typeof translations.pl)["nav"]> = [
|
const linksOrder: Array<keyof (typeof translations.pl)["nav"]> = [
|
||||||
'about',
|
'about',
|
||||||
'tickets',
|
'tickets',
|
||||||
'cfp',
|
'contribute',
|
||||||
'details',
|
'details',
|
||||||
|
'wiki',
|
||||||
'contact'
|
'contact'
|
||||||
]
|
]
|
||||||
|
|
||||||
export function MainpageNav({
|
export function MainpageNav({
|
||||||
t,
|
t,
|
||||||
|
currentLocale = 'pl' as Lang
|
||||||
}: {
|
}: {
|
||||||
t: typeof translations.pl
|
t: typeof translations.pl
|
||||||
|
currentLocale?: Lang
|
||||||
}) {
|
}) {
|
||||||
const parent = useRef<HTMLDivElement>(null);
|
const externalLinks: Record<string, string> = {
|
||||||
useColorSections(parent);
|
'wiki': getWikiUrl(currentLocale)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavContainer title={t.nav.title}>
|
<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) => (
|
{linksOrder.map((value) => {
|
||||||
<a
|
const isExternal = value in externalLinks;
|
||||||
key={value}
|
const href = isExternal ? externalLinks[value] : `#${value}`;
|
||||||
href={`#${value}`}
|
|
||||||
className="text-sm md:text-md hover:text-primary transition-colors relative group will-change-[color]"
|
return (
|
||||||
>
|
<a
|
||||||
{t.nav[value]}
|
key={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", {
|
href={href}
|
||||||
})} />
|
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||||
</a>
|
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>
|
||||||
<div className="md:hidden ml-2">
|
<div className="md:hidden ml-2">
|
||||||
<MobileNav t={t} linksOrder={linksOrder} />
|
<MobileNav t={t} linksOrder={linksOrder} externalLinks={externalLinks} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</NavContainer>
|
</NavContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,3 +159,11 @@
|
||||||
section {
|
section {
|
||||||
scroll-margin-top: calc(var(--spacing) * 16 + var(--spacing) * 4);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { Sections } from "@/i18n/translations";
|
|
||||||
import { useLayoutEffect, useRef } from "react";
|
|
||||||
|
|
||||||
export const linksOrder: Array<Sections> = [
|
|
||||||
"about",
|
|
||||||
"tickets",
|
|
||||||
"cfp",
|
|
||||||
"details",
|
|
||||||
"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]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -10,9 +10,10 @@ const pl = {
|
||||||
title: "CEBULACAMP",
|
title: "CEBULACAMP",
|
||||||
about: "O wydarzeniu",
|
about: "O wydarzeniu",
|
||||||
tickets: "Bilety",
|
tickets: "Bilety",
|
||||||
cfp: "Zgłoś prelekcję",
|
contribute: "Agenda",
|
||||||
details: "FAQ",
|
details: "FAQ",
|
||||||
contact: "Kontakt",
|
contact: "Kontakt",
|
||||||
|
wiki: "Wiki",
|
||||||
},
|
},
|
||||||
mobileNav: {
|
mobileNav: {
|
||||||
toggleMenu: "Aktywuj menu",
|
toggleMenu: "Aktywuj menu",
|
||||||
|
|
@ -43,15 +44,15 @@ const pl = {
|
||||||
},
|
},
|
||||||
tickets: {
|
tickets: {
|
||||||
title: "Bilety",
|
title: "Bilety",
|
||||||
status: `Rusza sprzedaż pierwszej puli biletów na Cebula Camp 2025: Reaktywacja!
|
status: `Bilety na Cebula Camp 2025: Reaktywacja zostały wyprzedane!`,
|
||||||
Od 20 kwietnia 2025 r. będzie można kupić bilet. Każdy z sześciu polskich Hackerspaceów ma voucher, który pozwala na zakup 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",
|
|
||||||
},
|
},
|
||||||
cfp: {
|
contribute: {
|
||||||
title: "Zgłoś prelekcję",
|
title: "Zgłoś się!",
|
||||||
status: "Wkrótce ™",
|
agenda: {
|
||||||
|
title: "Agenda",
|
||||||
|
description: "Zobacz pełny harmonogram wydarzeń, prelekcji i warsztatów",
|
||||||
|
viewButton: "Zobacz agendę",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
faq: {
|
faq: {
|
||||||
accommodation: {
|
accommodation: {
|
||||||
|
|
@ -76,11 +77,18 @@ W miejscu, gdzie będzie nasz Camp, mamy do dyspozycji przestrzeń, którą wyko
|
||||||
title: "Dokumenty",
|
title: "Dokumenty",
|
||||||
rules: "Regulamin wydarzenia",
|
rules: "Regulamin wydarzenia",
|
||||||
privacyPolicy: "Polityka prywatności",
|
privacyPolicy: "Polityka prywatności",
|
||||||
|
wiki: "Wiki CebulaCamp",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
title: "Kontakt",
|
title: "Kontakt",
|
||||||
email: common.orgaEmail,
|
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: {
|
credits: {
|
||||||
title: "Uznania",
|
title: "Uznania",
|
||||||
|
|
@ -110,9 +118,10 @@ const en = {
|
||||||
title: "CEBULACAMP",
|
title: "CEBULACAMP",
|
||||||
about: "About the event",
|
about: "About the event",
|
||||||
tickets: "Tickets",
|
tickets: "Tickets",
|
||||||
cfp: "Call for papers",
|
contribute: "Schedule",
|
||||||
details: "FAQ",
|
details: "FAQ",
|
||||||
contact: "Contact",
|
contact: "Contact",
|
||||||
|
wiki: "Wiki",
|
||||||
},
|
},
|
||||||
mobileNav: {
|
mobileNav: {
|
||||||
toggleMenu: "Toggle menu",
|
toggleMenu: "Toggle menu",
|
||||||
|
|
@ -142,16 +151,15 @@ const en = {
|
||||||
},
|
},
|
||||||
tickets: {
|
tickets: {
|
||||||
title: "Tickets",
|
title: "Tickets",
|
||||||
status: `The sale of the first batch of tickets for Cebula Camp 2025: Reactivation is starting!
|
status: `Tickets for Cebula Camp 2025: Reactivated are sold out!`,
|
||||||
|
|
||||||
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 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!",
|
|
||||||
},
|
},
|
||||||
cfp: {
|
contribute: {
|
||||||
title: "Call for papers",
|
title: "Contribute!",
|
||||||
status: "Soon ™",
|
agenda: {
|
||||||
|
title: "Schedule",
|
||||||
|
description: "View the full schedule of events, talks, and workshops",
|
||||||
|
viewButton: "View agenda",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
faq: {
|
faq: {
|
||||||
accommodation: {
|
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",
|
title: "Documents",
|
||||||
rules: "Terms and Conditions",
|
rules: "Terms and Conditions",
|
||||||
privacyPolicy: "Privacy Policy",
|
privacyPolicy: "Privacy Policy",
|
||||||
|
wiki: "CebulaCamp Wiki",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
title: "Contact",
|
title: "Contact",
|
||||||
email: common.orgaEmail,
|
email: common.orgaEmail,
|
||||||
|
details: {
|
||||||
|
line1: `Got questions, cool ideas, or just want to say hi?`,
|
||||||
|
line2: `— we're listening.`,
|
||||||
|
line3: `Whether you’re 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: {
|
credits: {
|
||||||
title: "Credits",
|
title: "Credits",
|
||||||
|
|
|
||||||
35
src/lib/external-links.ts
Normal file
35
src/lib/external-links.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
17. Organizator zastrzega sobie prawo do zmiany Regulaminu. Zmiany w regulaminie będą komunikowane drogą mailową oraz dostępne na stronie.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue