diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a70d08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "i18n-ally.localesPaths": ["messages", "src/lib/locale"] +} diff --git a/bun.lockb b/bun.lockb index 1ed5e7e..60db4b4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/messages/en.json b/messages/en.json index f9b2767..4118960 100644 --- a/messages/en.json +++ b/messages/en.json @@ -36,7 +36,7 @@ "navigationMenu": "Navigation menu", "news": "News", "events": "Events", - "storage": "Storage", + "storage": "Lager", "about": "About", "shiftSchedule": "Shift Schedule", "changeLocale": "Change language", @@ -71,6 +71,121 @@ "readTime": "{count, plural, =0 {less than a minute} one {# minute} other {# minutes}} read", "views": "Views" }, + "about": { + "whatIsHackerspace": "What is Hackerspace?", + "aboutDescription": " Hackerspace NTNU is a student-driven project open to all students, regardless of study direction or Hackerspace membership. We offer a creative arena where students from different disciplines can get help realizing their ideas in an engaged and inclusive environment. Here, you will find new technology at your disposal, including drones, 3D printers, and Virtual Reality equipment. Whether you are a first-year student needing help with your first Arduino project or a fourth-year student wanting to create a 3D model of Trondheim, we can provide both equipment and expertise. We also regularly hold courses for both beginners and advanced students in many exciting subjects. Come by to see what we're doing and have a chat. You can find us on the second floor of the A-block in the Realfagbygget at NTNU Gløshaugen.", + "showMap": "Show map!", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "canIUseThe3dPrinter": "Can I use the 3D printer?", + "answerCanIUseThe3dPrinter": " All students can use the 3D printers and the rest of the workshop, regardless of whether they are members of Hackerspace or not. We have a reservation system available for all students with FEIDE login from the website's navigation bar. Come by during opening hours and get help from one of our members.", + "canITryVRGames-Equipment": "Can I try VR games/equipment?", + "answerCanITryVRGames-Equipment": "Our VR equipment can be borrowed and tried out by all students, regardless of Hackerspace membership. We have a dedicated VR room where you can come and play as long as a Hackerspace member is available for setup and teardown. The equipment can be borrowed through the usual inventory system. Come by during our opening hours if you're interested.", + "howDoIBecomeAMember": "How do I become a member?", + "answerHowDoIBecomeAMember": " Hackerspace has ongoing admissions. You can apply whenever you want. We usually respond within 3 working weeks. Note that you do not need membership to borrow equipment or try something cool. Come by for a chat during our opening hours to learn more about our offerings and what it's like to be a member. " + }, + "activeGroup": "Our Active Groups", + "leaderboard": { + "title": "Leaderboard", + "about": "The leadership consists of a Leader, Deputy Leader, and Financial Officer in Hackerspace. They ensure that Hackerspace NTNU operates optimally and make decisions and planning for operations with the board.", + "leader": "Leader", + "deputyLeader": "Deputy Leader", + "financialManager": "Financial Manager" + }, + "memberRepresentative": { + "title": "Member representative", + "about": " Hackerspace shall have a representative who is appointed during the general assembly. This representative has no voting rights on the board but is a contact person for Hackerspace members regarding issues they do not wish to address directly with the board. The representative has a duty of confidentiality and will, if necessary, relay issues anonymized to the board. If you wish to get in touch, the representative is available on Slack. ", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + } + }, + "devops": { + "title": "DevOps", + "about": "The main tasks of DevOps are to develop and manage Hackerspace's website, but we also work on the operation of infrastructure, databases, servers, and are responsible for various gadgets.", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leader", + "deputyLeader": "Deputy Leader", + "member": "Member" + }, + "labops": { + "title": "LabOps", + "about": "The main tasks of LabOps are to run Hackerspace's premises, plan and carry out events, hold stands and run other promotions.", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "hei?", + "answer1": "hallo <3", + "question2": "hei?", + "answer2": "hallo <3", + "question3": "hei?", + "answer3": "hallo <3" + }, + "leader": "Leader", + "deputyLeader": "Deputy Leader", + "member": "Member" + }, + "breadboard-computer-group": { + "title": " Project: Breadboard Computer", + "about": "", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leader", + "deputyLeader": "Deputy Leader", + "member": "Member" + }, + "game-group": { + "title": "Project: game", + "about": "", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leader", + "deputyLeader": "Deputy Leader", + "member": "Member" + }, + "ttrpg-group": { + "title": "Project: ttrpg", + "about": "", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leader", + "deputyLeader": "Deputy Leader", + "member": "Member" + } + }, "storage": { "title": "Storage", "searchPlaceholder": "Search for product...", diff --git a/messages/no.json b/messages/no.json index 8062933..d167f19 100644 --- a/messages/no.json +++ b/messages/no.json @@ -71,6 +71,121 @@ "readTime": "{count, plural, =0 {mindre enn ett minutt} one {# minutt} other {# minutter}} lesing", "views": "Visninger" }, + "about": { + "whatIsHackerspace": "Hva er Hackerspace?", + "aboutDescription": " Hackerspace NTNU er et studentdrevet prosjekt åpent for alle studenter uansett studieretning eller Hackerspace-medlemsskap. Vi tilbyr en kreativ arena der studenter fra forskjellige linjer kan få hjelp til å realisere idéene sine i et engasjert og inkluderende miljø. Hos oss finner du ny teknologi til din disposisjon, blant annet droner, 3D-printere og Virtual Reality-utstyr. Om du er en førsteklassing som trenger hjelp med ditt første Arduino-prosjekt eller en fjerdeklassing som ønsker å lage en 3D-modell av Trondheim, kan vi stille med både utstyr og kompetanse. Vi holder også regelmessig kurs for både nybegynnere og viderekomne innen mange spennende emner. Kom innom for å se hva vi driver med og slå av en prat. Du finner oss i andre etasje i A-blokka på Realfagbygget, NTNU Gløshaugen. ", + "showMap": "Vis kart!", + "FAQ": { + "FAQ-title": "Ofte stilte spørsmål", + "canIUseThe3dPrinter": "Kan jeg bruke 3D-printer?", + "answerCanIUseThe3dPrinter": " Alle studenter kan benytte 3D-printere og resten av verkstedet uavhengig av om de er medlem av Hackerspace eller ikke. Vi har et reservasjonssystem tilgjengelig for alle studenter med FEIDE-pålogging fra nettsidens navigasjonsbar. Kom innom i åpningstiden og få hjelp av en av våre medlemmer.", + "canITryVRGames-Equipment": "Kan jeg prøve VR spill/utstyr?", + "answerCanITryVRGames-Equipment": "VR utstyret vårt kan lånes og prøves ut av alle studenter uavhengig av medlemskap i Hackerspace. Vi har eget VR-rom der man kan komme og spille så lenge et Hackerspace-medlem er tilgjengelig ved oppsett og avslutning. Utstyret kan lånes ut på vanlig måte gjennom lagersystemet. Kom innom i våre åpningstider om dette interesserer deg.", + "howDoIBecomeAMember": "Hvordan blir jeg medlem?", + "answerHowDoIBecomeAMember": " Hackerspace har løpende opptak. Du kan altså søke når du vil. Vi svarer som regel i løpet av 3 arbeidsuker. Merk at du ikke trenger medlemsskap for å låne utstyr eller prøve noe kult. Kom innom for en prat i våre åpningstider for å finne ut mer om vårt tilbud og hvordan det er å være medlem. " + }, + "activeGroup": "Våre aktive grupper", + "leaderboard": { + "title": "Ledelsen", + "about": "Ledelsen består av Leder, Nestleder og Økonomiansvarlig i Hackerspace. Disse sørger for at Hackerspace NTNU driftes optimalt og gjennomfører avgjørelser og planlegging av drift med styret.", + "leader": "Leder", + "deputyLeader": "Nestleder", + "financialManager": "Økonomiansvarlig" + }, + "memberRepresentative": { + "title": "Tillitsvalgt", + "about": " Hackerspace skal ha en tillitsvalgt som blir utnevnt under generalforsamlingen. Denne tillitsvalgte har ingen stemme i styret, men er en kontaktperson for medlemmer av Hackerspace for saker de ikke ønsker å ta direkte opp med styret. Tillitsvalgt har taushetsplikt, og vil ved behov videreformidle saker anonymisert til styret. Dersom du ønsker å ta kontakt er tillitsvalgt tilgjengelig på Slack. ", + "FAQ": { + "FAQ-title": "Ofte stilte spørsmål", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + } + }, + "devops": { + "title": "DevOps", + "information": "Hovedoppgavene til DevOps er å utvikle og drifte Hackerspace sin nettside, men vi jobber også med drift av infrastruktur, databaser, servere, og har ansvar for diverse dingser.", + "FAQ": { + "FAQ-title": "Ofte stilte spørsmål", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leder", + "deputyLeader": "Nestleder", + "member": "Medlemmer" + }, + "labops": { + "title": "LabOps", + "about": "Hovedoppgavene til LabOps er å drifte Hackerspace sine lokaler, planlegge og gjennomføre arrangement, holde stands og drive annen promotering.", + "FAQ": { + "FAQ-title": "Ofte stilte spørsmål", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leder", + "deputyLeader": "Nestleder", + "member": "Medlemmer" + }, + "breadboard-computer-group": { + "title": "Prosjekt: Breadboard Computer", + "about": "", + "FAQ": { + "FAQ-title": "Ofte stilte spørsmål", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leder", + "deputyLeader": "Nestleder", + "member": "Medlemmer" + }, + "game-group": { + "title": "Prosjekt: spill", + "about": "", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leder", + "deputyLeader": "Nestleder", + "member": "Medlemmer" + }, + "ttrpg-group": { + "title": "Prosjekt: ttrpg", + "about": "", + "FAQ": { + "FAQ-title": "Frequently Asked Questions", + "question1": "", + "answer1": "", + "question2": "", + "answer2": "", + "question3": "", + "answer3": "" + }, + "leader": "Leder", + "deputyLeader": "Nestleder", + "member": "Medlemmer" + } + }, "storage": { "title": "Lager", "searchPlaceholder": "Søk etter produkt...", diff --git a/package.json b/package.json index a496c1d..49caf2f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.679.0", "@lucia-auth/adapter-drizzle": "^1.1.0", + "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.1", @@ -45,7 +46,9 @@ "cva": "^1.0.0-beta.1", "date-fns": "^4.1.0", "drizzle-orm": "^0.33.0", - "lucia": "3.2.0", + "embla-carousel-autoplay": "^8.5.2", + "embla-carousel-react": "^8.5.2", + "lucia": "^3.2.0", "lucide-react": "^0.396.0", "next": "^15.0.1", "next-intl": "^3.23.5", diff --git a/public/about/pizzaWolfs-min.png b/public/about/pizzaWolfs-min.png new file mode 100644 index 0000000..8d42c72 Binary files /dev/null and b/public/about/pizzaWolfs-min.png differ diff --git a/src/app/[locale]/(default)/about/LabOps/layout.tsx b/src/app/[locale]/(default)/about/LabOps/layout.tsx new file mode 100644 index 0000000..a28cd17 --- /dev/null +++ b/src/app/[locale]/(default)/about/LabOps/layout.tsx @@ -0,0 +1,25 @@ +"use client"; + +import {ImageCarousel} from '@/components/about/ImageCarousel'; + +type CarouselLayoutProps = { + locale: string; + children: React.ReactNode; +}; +const images = [ + { id: "image1", src: "/unknown.png", alt: "unknown" }, + { id: "image2", src: "/mock.jpg", alt: "mock" }, + { id: "image3", src: "/bg.jpg", alt: "bg" }, + { id: "image4", src: "/about/pizzaWolfs-min.png", alt: "pizzaWolfs" }, + ]; + +export default function CarouselLayout({ locale, children }: CarouselLayoutProps) { + return ( +
+
+ +
+
{children}
+
+ ); +} diff --git a/src/app/[locale]/(default)/about/LabOps/page.tsx b/src/app/[locale]/(default)/about/LabOps/page.tsx new file mode 100644 index 0000000..84331a2 --- /dev/null +++ b/src/app/[locale]/(default)/about/LabOps/page.tsx @@ -0,0 +1,28 @@ +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; +import { FAQAccordion } from '@/components/about/FAQAccordion'; +import { MembersTable } from '@/components/about/MembersTable'; + +export default function labopsPage({ + params: { locale }, + }: { + params: { locale: string}; + }) { + unstable_setRequestLocale(locale); + const t = useTranslations('about.labops'); + + const faqs = [ + { id: 'faq1', question: t('FAQ.question1'), answer: t('FAQ.answer1') }, + { id: 'faq2', question: t('FAQ.question2'), answer: t('FAQ.answer2') }, + { id: 'faq3', question: t('FAQ.question3'), answer: t('FAQ.answer3') }, + ]; + + return ( +
+

{t('title')}

+

{t('about')}

+ + +
+ ); + } \ No newline at end of file diff --git a/src/app/[locale]/(default)/about/breadboard-computer/page.tsx b/src/app/[locale]/(default)/about/breadboard-computer/page.tsx new file mode 100644 index 0000000..e42d7e0 --- /dev/null +++ b/src/app/[locale]/(default)/about/breadboard-computer/page.tsx @@ -0,0 +1,18 @@ +import { useTranslations } from 'next-intl'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; + + + +export default function BreadBoardComputerPage({ + params: { locale }, + }: { + params: { locale: string }; + }) { + unstable_setRequestLocale(locale); + const t = useTranslations('about.breadboard-computer-group') + + return ( +
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(default)/about/devops/layout.tsx b/src/app/[locale]/(default)/about/devops/layout.tsx new file mode 100644 index 0000000..391dabc --- /dev/null +++ b/src/app/[locale]/(default)/about/devops/layout.tsx @@ -0,0 +1,25 @@ +"use client"; + +import {ImageCarousel} from '@/components/about/ImageCarousel'; + +type CarouselLayoutProps = { + locale: string; + children: React.ReactNode; +}; +const images = [ + { id: "image1", src: "/unknown.png", alt: "unknown"}, + { id: "image2", src: "/mock.jpg", alt: "mock"}, + { id: "image3", src: "/bg.jpg", alt: "bg"}, + { id: "image4", src: "/about/pizzaWolfs-min.png", alt: "pizzaWolfs"}, + ]; + +export default function CarouselLayout({ locale, children }: CarouselLayoutProps) { + return ( +
+
+ +
+
{children}
+
+ ); +} diff --git a/src/app/[locale]/(default)/about/devops/page.tsx b/src/app/[locale]/(default)/about/devops/page.tsx new file mode 100644 index 0000000..9376428 --- /dev/null +++ b/src/app/[locale]/(default)/about/devops/page.tsx @@ -0,0 +1,38 @@ +import { Card } from '@/components/ui/Card'; +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; + +export default function devopsPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + unstable_setRequestLocale(locale); + const t = useTranslations('about.devops'); + + return ( +
+

DevOps

+
{t('about')}
+
+ +
+
+
+
+
+

Leder

+ leder +
+
+

Nestleder

+ nestleder +
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(default)/about/game/page.tsx b/src/app/[locale]/(default)/about/game/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/[locale]/(default)/about/leaderboard/page.tsx b/src/app/[locale]/(default)/about/leaderboard/page.tsx new file mode 100644 index 0000000..22216ba --- /dev/null +++ b/src/app/[locale]/(default)/about/leaderboard/page.tsx @@ -0,0 +1,88 @@ +import { Button } from '@/components/ui/Button'; +import { Card, CardFooter, CardHeader } from '@/components/ui/Card'; +import { unstable_setRequestLocale } from 'next-intl/server'; +import { useTranslations } from 'next-intl'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/Accordion'; + +export default function LeaderboardPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + unstable_setRequestLocale(locale); + const t = useTranslations('about.leaderboard'); + + + return ( +
+

{t('title')}

+
+

+ {t('about')} +

+
+
+

FAQ's

+ + + +

hei?

+
+ + hallo + +
+ + + +

hei?

+
+ + hallo + +
+ + + +

hei?

+
+ + hallo + +
+
+
+
+
+
+

{t('leader')}

+ + leder {/* use medlem-components instead. */} + + + + +
+
+

{t('deputyLeader')}

+ + Nestleder + + + + +
+
+

{t('financialManager')}

+ + Økonomiansvarlig + + + + +
+
+
+
+ ); +} diff --git a/src/app/[locale]/(default)/about/member-representative/page.tsx b/src/app/[locale]/(default)/about/member-representative/page.tsx new file mode 100644 index 0000000..d8ee64b --- /dev/null +++ b/src/app/[locale]/(default)/about/member-representative/page.tsx @@ -0,0 +1,40 @@ +import { Card, CardFooter } from '@/components/ui/Card'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/Accordion'; +import { useTranslations } from 'next-intl'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { FAQAccordion } from '@/components/about/FAQAccordion'; + +export default function MemberRepresentativePage({ + params: { locale }, + }: { + params: { locale: string }; + }) { + unstable_setRequestLocale(locale); + const t = useTranslations('member-representative') + + return ( +
+

Member Representative

+
+ {t.rich('information', { + p1: (chunks) =>

{chunks}

, + p2: (chunks) =>

{chunks}

+ })} +
+
+
+ + hihi + member-card av tillietsvalgt + +
+
+

FAQ'S

+

faq accordion

+ +
+
+
+ ); + } + \ No newline at end of file diff --git a/src/app/[locale]/(default)/about/page.tsx b/src/app/[locale]/(default)/about/page.tsx index f21eabd..d8e4373 100644 --- a/src/app/[locale]/(default)/about/page.tsx +++ b/src/app/[locale]/(default)/about/page.tsx @@ -1,25 +1,102 @@ -import { getTranslations, setRequestLocale } from 'next-intl/server'; +import { getTranslations } from 'next-intl/server'; +import React from 'react'; +import { Card, CardContent, CardHeader } from 'src/components/ui/Card'; +import { Printer, Gamepad2, SquareUserRound } from 'lucide-react'; +import { Meteors } from '@/components/ui/Meteor'; +import { FAQAccordion } from '@/components/about/FAQAccordion'; +import Image from 'next/image'; +import pizzaWolfs from 'public/about/pizzaWolfs-min.png'; +import { GroupCardGrid } from '@/components/about/GroupCardGrid'; -export async function generateMetadata({ +export default async function AboutPage({ params, }: { params: Promise<{ locale: string }>; }) { const { locale } = await params; + const t = await getTranslations('about'); + const tFAQ = await getTranslations('about.FAQ') - const t = await getTranslations({ locale, namespace: 'layout' }); +const cardData = [ + { id: 1, title: t('leaderboard.title'), content: t('leaderboard.about'), link: `/${locale}/about/leaderboard`}, + { id: 2, title: t('memberRepresentative.title'), + content: t.rich('memberRepresentative.about', { + p1: (chunks) =>

{chunks}

, + p2: (chunks) =>

{chunks}

+ }), + link: `/${locale}/about/leaderboard`}, + { id: 3, title: t('devops.title'), content: t('devops.about'), link: `/${locale}/about/leaderboard`}, + { id: 4, title: t('labops.title'), content: t('labops.about'), link: `/${locale}/about/leaderboard`}, + { id: 5, title: t('breadboard-computer-group.title'), content: t('breadboard-computer-group.about'), link: `/${locale}/about/labops`}, + { id: 6, title: t('game-group.title'), content: t('game-group.about'), link: `/${locale}/about/devops`}, + { id: 7, title: t('ttrpg-group.title'), content: t('ttrpg-group.about'), link: ''}, +]; - return { - title: t('about'), - }; -} +/* const faqs: FAQ[] = [ + { + id: 'item-1', + icon: , + question: tFAQ('canIUseThe3dPrinter'), + answer: tFAQ.rich('answerCanIUseThe3dPrinter', { + p1: (chunks) =>

{chunks}

, + p2: (chunks) =>

{chunks}

+ }) + }, + { + id: 'item-2', + icon: , + question: tFAQ('canITryVRGames-Equipment'), + answer: tFAQ('answerCanITryVRGames-Equipment') + }, + { + id: 'item-3', + icon: , + question: tFAQ('howDoIBecomeAMember'), + answer: tFAQ.rich('answerHowDoIBecomeAMember', { + p1: (chunks) =>

{chunks}

, + p2: (chunks) =>

{chunks}

+ }) + } +]; */ -export default async function AboutPage({ - params, -}: { - params: Promise<{ locale: string }>; -}) { - const { locale } = await params; - setRequestLocale(locale); - return
this should be about page
; -} + return
+
+ pizza wolfs +
+
+

{t('whatIsHackerspace')}

+
+ {t.rich('aboutDescription', { + p1: (chunks) =>

{chunks}

, + p2: (chunks) =>

{chunks}

, + p3: (chunks) =>

{chunks}

+ })} +
+
+ {/* SKAL LEGGES I BUNNEN AV LANDINGPAGE? */} + +

{t('activeGroup')}

+ + + {/*
+ {cardData.map(card => ( + +
+ + {card.title} + {card.content} +
+ +
+
+
+ ))} +
*/} +
; +} \ No newline at end of file diff --git a/src/app/[locale]/(default)/about/ttrpg/page.tsx b/src/app/[locale]/(default)/about/ttrpg/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/about/FAQAccordion.tsx b/src/components/about/FAQAccordion.tsx new file mode 100644 index 0000000..12eb986 --- /dev/null +++ b/src/components/about/FAQAccordion.tsx @@ -0,0 +1,47 @@ +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/Accordion'; + +type FAQ = { + id: string; + question: string; + answer: string; + + icon?: React.ReactNode; +}; + +type FAQAccordionProps = { + faqs: FAQ[]; +}; + +function FAQAccordion({ faqs }: FAQAccordionProps) { + return ( +
+

FAQ'S

+ + {faqs.map((faq) => ( + + +
+ {faq.icon && {faq.icon}} + {faq.question} +
+
+ + {faq.answer} + +
+ ))} +
+
+ ); +} + +export { FAQAccordion }; diff --git a/src/components/about/GroupCard.tsx b/src/components/about/GroupCard.tsx new file mode 100644 index 0000000..bd6c3c0 --- /dev/null +++ b/src/components/about/GroupCard.tsx @@ -0,0 +1,74 @@ +import { Meteors } from '@/components/ui/Meteor'; +import { Link } from '@/lib/locale/navigation'; +import { cx } from '@/lib/utils'; +import Image from 'next/image'; + +import { Button } from '@/components/ui/Button'; +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/Card'; + +type GroupCardProps = { + className?: string; + id: number; + name: string; + photoUrl: string; + description: string; +}; + +function GroupCard({ + className, + id, + name, + photoUrl, + description, +}: GroupCardProps) { + return ( + + ); +} + +export { GroupCard, type GroupCardProps }; diff --git a/src/components/about/GroupCardGrid.tsx b/src/components/about/GroupCardGrid.tsx new file mode 100644 index 0000000..a23f08c --- /dev/null +++ b/src/components/about/GroupCardGrid.tsx @@ -0,0 +1,20 @@ +import { groupMockData as groupData } from '@/mock-data/groups'; +import { GroupCard } from './GroupCard'; + +function GroupCardGrid() { + return ( +
+ {groupData.map((data) => ( + + ))} +
+ ); +} + +export { GroupCardGrid }; diff --git a/src/components/about/ImageCarousel.tsx b/src/components/about/ImageCarousel.tsx new file mode 100644 index 0000000..6e4b3b4 --- /dev/null +++ b/src/components/about/ImageCarousel.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { + Carousel, + CarouselContent, + CarouselItem, +} from '@/components/ui/Carousel'; +import Autoplay from 'embla-carousel-autoplay'; +import Image from 'next/image'; +import React from 'react'; + +type images = { + id: string; + src: string; + alt: string; +}; + +type ImageCarouselProps = { + images: images[]; +}; + +function ImageCarousel({ images }: ImageCarouselProps) { + const plugin = React.useRef( + Autoplay({ delay: 2000, stopOnInteraction: true }), + ); + + return ( +
+ + + {images.map((image) => ( + +
+ {image.alt} +
+
+ ))} +
+
+
+ ); +} +export { ImageCarousel }; diff --git a/src/components/about/LeadershipCard.tsx b/src/components/about/LeadershipCard.tsx new file mode 100644 index 0000000..d7e09c4 --- /dev/null +++ b/src/components/about/LeadershipCard.tsx @@ -0,0 +1,17 @@ +type LeadershipCardProps = { + className?: string; + name: string; + leader: string; + deputyLeader: string; + headOfEconomy: string; +}; + +function LeadershipCard({ + className, + name, + leader, + deputyLeader, + headOfEconomy, +}: LeadershipCardProps) { + return
hei
; +} diff --git a/src/components/about/MembersTable.tsx b/src/components/about/MembersTable.tsx new file mode 100644 index 0000000..cc2224b --- /dev/null +++ b/src/components/about/MembersTable.tsx @@ -0,0 +1,30 @@ +import { + Table, + TableBody, + TableCaption, + TableRow, +} from '@/components/ui/Table'; +import { useTransition } from 'react'; + +type Member = { + name: string; + image: string; +}; + +type MembersTableProps = { + Members: Member[]; +}; + +function MembersTable() { + return ( +
+ + + name of a member + +
+
+ ); +} + +export { MembersTable }; diff --git a/src/components/ui/Accordion.tsx b/src/components/ui/Accordion.tsx new file mode 100644 index 0000000..ade801c --- /dev/null +++ b/src/components/ui/Accordion.tsx @@ -0,0 +1,58 @@ +'use client'; + +import * as AccordionPrimitive from '@radix-ui/react-accordion'; +import { ChevronDown } from 'lucide-react'; +import * as React from 'react'; + +import { cx } from '@/lib/utils'; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = 'AccordionItem'; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/components/ui/Carousel.tsx b/src/components/ui/Carousel.tsx new file mode 100644 index 0000000..7a39fe0 --- /dev/null +++ b/src/components/ui/Carousel.tsx @@ -0,0 +1,262 @@ +'use client'; + +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from 'embla-carousel-react'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; +import * as React from 'react'; + +import { Button } from '@/components/ui/Button'; +import { cx } from '@/lib/utils'; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = 'horizontal', + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref, + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api?.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = 'Carousel'; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = 'CarouselContent'; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = 'CarouselItem'; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = 'CarouselPrevious'; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = 'CarouselNext'; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/src/components/ui/Meteor.tsx b/src/components/ui/Meteor.tsx new file mode 100644 index 0000000..5eff7a5 --- /dev/null +++ b/src/components/ui/Meteor.tsx @@ -0,0 +1,38 @@ +import { cx } from '@/lib/utils'; +import React from 'react'; +import { useId } from 'react'; + +export const Meteors = ({ + number, + className, + hoverDelay, +}: { + number?: number; + className?: string; + hoverDelay?: string; +}) => { + const meteors = new Array(number || 20).fill(null).map(() => useId()); + + return ( + <> + {meteors.map((id) => ( + + ))} + + ); +}; diff --git a/src/lib/locale/index.ts b/src/lib/locale/index.ts index 0166353..50565eb 100644 --- a/src/lib/locale/index.ts +++ b/src/lib/locale/index.ts @@ -35,6 +35,10 @@ const routing = defineRouting({ en: '/about', no: '/om-oss', }, + '/about/[group]': { + en: '/about/[group]', + no: '/om-oss/[group]', + }, '/storage': { en: '/storage', no: '/lager', diff --git a/src/mock-data/groups.ts b/src/mock-data/groups.ts new file mode 100644 index 0000000..bd0a116 --- /dev/null +++ b/src/mock-data/groups.ts @@ -0,0 +1,39 @@ +const groupMockData = [ + { + id: 1, + name: 'DevOps', + photoUrl: 'mock.jpg', + description: + 'We do all the cool techy tech stuff, aka the backbone, the spirit and soul of Hackerspace', + }, + { + id: 2, + name: 'LabOps', + photoUrl: 'mock.jpg', + description: + 'We do all the cool techy tech stuff, aka the backbone, the spirit and soul of Hackerspace', + }, + { + id: 3, + name: 'Breadboard Computer', + photoUrl: 'mock.jpg', + description: + 'We do all the cool techy tech stuff, aka the backbone, the spirit and soul of Hackerspace', + }, + { + id: 4, + name: 'Spill', + photoUrl: 'mock.jpg', + description: + 'We do all the cool techy tech stuff, aka the backbone, the spirit and soul of Hackerspace', + }, + { + id: 5, + name: 'TTRPG', + photoUrl: 'mock.jpg', + description: + 'We do all the cool techy tech stuff, aka the backbone, the spirit and soul of Hackerspace', + }, +]; + +export { groupMockData }; diff --git a/tailwind.config.ts b/tailwind.config.ts index 2b6533c..b73f434 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -76,6 +76,33 @@ const config: Config = { '160': '40rem', '192': '48rem', }, + + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + meteor: { + '0%': { + transform: 'rotate(215deg) translateX(0)', + opacity: '0', + }, + '25%': { opacity: '0.5' }, + '100%': { + transform: 'rotate(215deg) translateX(-500px)', + opacity: '1', + }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'meteor-effect': 'meteor 5s linear infinite', + }, }, }, plugins: [