Skip to content

Commit 71014b8

Browse files
authored
Merge pull request #147 from hackerspace-ntnu/quotes
feat: Add quotes page
2 parents 9363781 + a631e75 commit 71014b8

39 files changed

+3499
-172
lines changed

messages/en-GB.json

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"cancel": "Cancel",
3535
"save": "Save",
3636
"uploadText": "Drop files here or click to upload",
37-
"noValidFiles": "No valid files were dropped"
37+
"noValidFiles": "No valid files were dropped",
38+
"noChoicesFound": "No choices found"
3839
},
3940
"error": {
4041
"notFound": "404 - Page not found",
@@ -120,6 +121,7 @@
120121
"about": "About",
121122
"shiftSchedule": "Shift Schedule",
122123
"members": "Members",
124+
"quotes": "Quotes",
123125
"desktopNavMenu": "{open, select, true {Open} other {Close}} navigation menu",
124126
"goToMatrix": "Go to Matrix",
125127
"rules": "Rules",
@@ -920,13 +922,6 @@
920922
"description": "Manage banners displayed on the website."
921923
}
922924
},
923-
"api": {
924-
"tooManyRequests": "Too many requests, please try again later",
925-
"notAuthenticated": "Not authenticated",
926-
"notAuthorized": "Not authorized",
927-
"noFileFound": "No file found",
928-
"unableToUpdateMatrix": "Unable to update on Matrix"
929-
},
930925
"privacy": {
931926
"title": "Privacy Policy",
932927
"lastUpdated": "Last updated: 9th July 2025",
@@ -990,6 +985,69 @@
990985
"content": "We reserve the right to change this Privacy Policy at any time. Any changes will be published on this page and the change date will be updated. We encourage you to regularly read the statement to stay informed about how we protect your privacy."
991986
}
992987
},
988+
"quotes": {
989+
"title": "Inspirational Quotes",
990+
"backToQuotes": "Back to quotes",
991+
"heardByUser": "Heard by: {user}",
992+
"new": {
993+
"title": "New Quote",
994+
"createQuote": "Create quote",
995+
"success": "Quote created",
996+
"unauthorized": "Unauthorized to create new quotes"
997+
},
998+
"update": {
999+
"title": "Update Quote",
1000+
"updateQuote": "Update quote",
1001+
"success": "Quote updated",
1002+
"unauthorized": "Unauthorized to update quotes"
1003+
},
1004+
"delete": {
1005+
"title": "Are you sure you want to delete this quote?",
1006+
"description": "This action cannot be undone.",
1007+
"success": "Quote deleted successfully",
1008+
"unauthorized": "Unauthorized to delete quotes"
1009+
},
1010+
"form": {
1011+
"userId": {
1012+
"label": "User",
1013+
"required": "User is required",
1014+
"placeholder": "Search for a user...",
1015+
"description": "Select a user"
1016+
},
1017+
"content": {
1018+
"labelEnglish": "Quote (English)",
1019+
"labelNorwegian": "Quote (Norwegian)",
1020+
"placeholderEnglish": "My heart cries because I do not own a 3D printer",
1021+
"placeholderNorwegian": "Mitt hjerte gråter fordi jeg ikke eier en 3D-printer",
1022+
"required": "Quote is required",
1023+
"minLength": "Quote must be at least {count} characters"
1024+
},
1025+
"internal": {
1026+
"label": "Internal quote",
1027+
"description": "If selected, this quote will only be visible to Hackerspace members."
1028+
},
1029+
"loadingMembers": "Loading members...",
1030+
"noMembersFound": "No members found"
1031+
},
1032+
"api": {
1033+
"invalidId": "Invalid ID",
1034+
"invalidOffset": "Invalid offset",
1035+
"insertFailed": "Failed to add quote",
1036+
"updateFailed": "Failed to update quote",
1037+
"deleteFailed": "Failed to delete quote",
1038+
"failedToFetchQuotes": "The server failed to get quotes",
1039+
"noUserWithUsername": "No user with provided username found",
1040+
"quoteNotFound": "Quote not found"
1041+
}
1042+
},
1043+
"api": {
1044+
"internalServerError": "Internal server error",
1045+
"tooManyRequests": "Too many requests, please try again later",
1046+
"notAuthenticated": "Not authenticated",
1047+
"notAuthorized": "Not authorized",
1048+
"noFileFound": "No file found",
1049+
"unableToUpdateMatrix": "Unable to update on Matrix"
1050+
},
9931051
"reservations": {
9941052
"title": "Reservations",
9951053
"backButton": "Back",

messages/nb-NO.json

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"cancel": "Avbryt",
3535
"save": "Lagre",
3636
"uploadText": "Slipp filer her eller klikk for å laste opp",
37-
"noValidFiles": "Ingen gyldige filer ble sluppet"
37+
"noValidFiles": "Ingen gyldige filer ble sluppet",
38+
"noChoicesFound": "Ingen valg funnet"
3839
},
3940
"error": {
4041
"notFound": "404 - Siden ble ikke funnet",
@@ -120,6 +121,7 @@
120121
"about": "Om oss",
121122
"shiftSchedule": "Vaktliste",
122123
"members": "Medlemmer",
124+
"quotes": "Sitater",
123125
"desktopNavMenu": "{open, select, true {Åpne} other {Lukk}} navigasjonsmeny",
124126
"goToMatrix": "Dra til Matrix",
125127
"rules": "Regler",
@@ -920,13 +922,6 @@
920922
"description": "Administrer bannere som vises på nettsiden."
921923
}
922924
},
923-
"api": {
924-
"tooManyRequests": "For mange forespørsler. Vennligst vent noen minutter og prøv igjen",
925-
"notAuthenticated": "Ikke autentisert",
926-
"notAuthorized": "Ikke autorisert",
927-
"noFileFound": "Ingen fil funnet",
928-
"unableToUpdateMatrix": "Kunne ikke oppdatere på Matrix"
929-
},
930925
"privacy": {
931926
"title": "Personvernerklæring",
932927
"lastUpdated": "Sist oppdatert: 9. juli 2025",
@@ -990,6 +985,69 @@
990985
"content": "Vi forbeholder oss retten til å endre denne personvernerklæringen når som helst. Eventuelle endringer vil bli publisert på denne siden, og endringsdatoen vil bli oppdatert. Vi oppfordrer deg til å regelmessig lese erklæringen for å holde deg informert om hvordan vi beskytter personvernet ditt."
991986
}
992987
},
988+
"quotes": {
989+
"title": "Inspirerende sitater",
990+
"backToQuotes": "Tilbake til sitater",
991+
"heardByUser": "Hørt av: {user}",
992+
"new": {
993+
"title": "Nytt sitat",
994+
"createQuote": "Opprett sitat",
995+
"success": "Sitat opprettet",
996+
"unauthorized": "Du har ikke rettigheter til å opprette nye sitater"
997+
},
998+
"update": {
999+
"title": "Oppdater sitat",
1000+
"updateQuote": "Oppdater sitat",
1001+
"success": "Sitat oppdatert",
1002+
"unauthorized": "Du har ikke rettigheter til å oppdatere sitater"
1003+
},
1004+
"delete": {
1005+
"title": "Er du sikker på at du vil slette dette sitatet?",
1006+
"description": "Denne handlingen kan ikke angres.",
1007+
"success": "Sitat slettet",
1008+
"unauthorized": "Du har ikke rettigheter til å slette sitater"
1009+
},
1010+
"form": {
1011+
"userId": {
1012+
"label": "Bruker",
1013+
"required": "Bruker er påkrevd",
1014+
"placeholder": "Søk etter en bruker...",
1015+
"description": "Velg en bruker"
1016+
},
1017+
"content": {
1018+
"labelEnglish": "Sitat (engelsk)",
1019+
"labelNorwegian": "Sitat (norsk)",
1020+
"placeholderEnglish": "My heart cries because I do not own a 3D printer",
1021+
"placeholderNorwegian": "Mitt hjerte gråter fordi jeg ikke eier en 3D-printer",
1022+
"required": "Sitat er påkrevd",
1023+
"minLength": "Sitat må være minst {count} tegn"
1024+
},
1025+
"internal": {
1026+
"label": "Internt sitat",
1027+
"description": "Hvis valgt, vil dette sitatet kun være synlig for Hackerspace-medlemmer."
1028+
},
1029+
"loadingMembers": "Laster inn medlemmer...",
1030+
"noMembersFound": "Ingen medlemmer funnet"
1031+
},
1032+
"api": {
1033+
"invalidId": "Ugyldig ID",
1034+
"invalidOffset": "Ugyldig offset",
1035+
"insertFailed": "Kunne ikke opprette sitat",
1036+
"updateFailed": "Kunne ikke oppdatere sitat",
1037+
"deleteFailed": "Kunne ikke slette sitat",
1038+
"failedToFetchQuotes": "Serveren klarte ikke å hente sitater",
1039+
"noUserWithUsername": "Ingen bruker med oppgitt brukernavn funnet",
1040+
"quoteNotFound": "Sitat ikke funnet"
1041+
}
1042+
},
1043+
"api": {
1044+
"internalServerError": "Intern serverfeil",
1045+
"tooManyRequests": "For mange forespørsler. Vennligst vent noen minutter og prøv igjen",
1046+
"notAuthenticated": "Ikke autentisert",
1047+
"notAuthorized": "Ikke autorisert",
1048+
"noFileFound": "Ingen fil funnet",
1049+
"unableToUpdateMatrix": "Kunne ikke oppdatere på Matrix"
1050+
},
9931051
"reservations": {
9941052
"title": "Reservasjoner",
9951053
"backButton": "Tilbake",

src/app/[locale]/(default)/about/group/[name]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default async function GroupPage({
8989
['labops', 'leadership', 'admin'].includes(g),
9090
) && (
9191
<Link
92-
className='absolute top-0 right-0'
92+
className='-translate-y-1/2 absolute top-1/2 right-0'
9393
href={{
9494
pathname: '/about/group/[name]/edit',
9595
params: { name: group.identifier },
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ArrowLeftIcon } from 'lucide-react';
2+
import { getTranslations } from 'next-intl/server';
3+
import { QuoteFormSkeleton } from '@/components/quotes/QuoteFormSkeleton';
4+
import { Link } from '@/components/ui/Link';
5+
6+
export default async function NewQuoteLoading() {
7+
const t = await getTranslations('quotes');
8+
const tUpdate = await getTranslations('quotes.update');
9+
10+
return (
11+
<>
12+
<Link
13+
className='flex w-fit items-center gap-2'
14+
href='/quotes'
15+
variant='ghost'
16+
size='default'
17+
>
18+
<ArrowLeftIcon />
19+
<span>{t('backToQuotes')}</span>
20+
</Link>
21+
<h1 className='text-center'>{tUpdate('title')}</h1>
22+
<QuoteFormSkeleton />
23+
</>
24+
);
25+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ArrowLeftIcon } from 'lucide-react';
2+
import { notFound } from 'next/navigation';
3+
import { type Locale, type Messages, NextIntlClientProvider } from 'next-intl';
4+
import {
5+
getMessages,
6+
getTranslations,
7+
setRequestLocale,
8+
} from 'next-intl/server';
9+
import { QuoteForm } from '@/components/quotes/QuoteForm';
10+
import { Link } from '@/components/ui/Link';
11+
import { api } from '@/lib/api/server';
12+
13+
export async function generateMetadata() {
14+
const t = await getTranslations('quotes.update');
15+
16+
return {
17+
title: t('title'),
18+
};
19+
}
20+
21+
export default async function NewQuotePage({
22+
params,
23+
}: {
24+
params: Promise<{ locale: Locale; quoteId: string }>;
25+
}) {
26+
const { locale, quoteId } = await params;
27+
setRequestLocale(locale);
28+
29+
const { user } = await api.auth.state();
30+
const t = await getTranslations('quotes');
31+
const tUpdate = await getTranslations('quotes.update');
32+
33+
const processedQuoteId = Number(quoteId);
34+
if (
35+
Number.isNaN(processedQuoteId) ||
36+
!Number.isInteger(processedQuoteId) ||
37+
processedQuoteId < 1
38+
)
39+
return notFound();
40+
41+
const quote = await api.quotes.fetchQuote(processedQuoteId);
42+
43+
if (!quote) return notFound();
44+
45+
if (
46+
!user?.groups.some((g) => ['labops', 'leadership', 'admin'].includes(g)) &&
47+
quote.saidBy.id !== user?.id &&
48+
quote.heardBy.id !== user?.id
49+
) {
50+
// TODO: Actually return a HTTP 401 Unauthorized reponse whenever `unauthorized.tsx` is stable
51+
throw new Error(tUpdate('unauthorized'));
52+
}
53+
54+
const { quotes, ui } = await getMessages();
55+
56+
const quoteUser = await api.users.fetchUser({ id: quote.saidBy.id });
57+
58+
return (
59+
<>
60+
<Link
61+
className='flex w-fit items-center gap-2'
62+
href='/quotes'
63+
variant='ghost'
64+
size='default'
65+
>
66+
<ArrowLeftIcon />
67+
<span>{t('backToQuotes')}</span>
68+
</Link>
69+
<h1 className='text-center'>{tUpdate('title')}</h1>
70+
<NextIntlClientProvider
71+
messages={{ quotes, ui } as Pick<Messages, 'quotes' | 'ui'>}
72+
>
73+
<QuoteForm
74+
quote={quote}
75+
initialUser={
76+
quoteUser && {
77+
id: quoteUser.id as number,
78+
profilePictureUrl: quoteUser.profilePictureUrl,
79+
firstName: quoteUser.firstName as string,
80+
lastName: quoteUser.lastName as string,
81+
profilePictureId: quoteUser.profilePictureId as number,
82+
}
83+
}
84+
/>
85+
</NextIntlClientProvider>
86+
</>
87+
);
88+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ArrowLeftIcon } from 'lucide-react';
2+
import { getTranslations } from 'next-intl/server';
3+
import { QuoteFormSkeleton } from '@/components/quotes/QuoteFormSkeleton';
4+
import { Link } from '@/components/ui/Link';
5+
6+
export default async function NewQuotePage() {
7+
const t = await getTranslations('quotes');
8+
const tNew = await getTranslations('quotes.new');
9+
10+
return (
11+
<>
12+
<Link
13+
className='flex w-fit items-center gap-2'
14+
href='/quotes'
15+
variant='ghost'
16+
size='default'
17+
>
18+
<ArrowLeftIcon />
19+
<span>{t('backToQuotes')}</span>
20+
</Link>
21+
<h1 className='text-center'>{tNew('title')}</h1>
22+
<QuoteFormSkeleton />
23+
</>
24+
);
25+
}

0 commit comments

Comments
 (0)