Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d5f928a
feat: create quotes pages and quotes table in database
michaelbrusegard Mar 13, 2025
a97f93e
feat: add quote validation
michaelbrusegard Mar 13, 2025
fe1ddb3
feat: add api router for quotes
michaelbrusegard Mar 13, 2025
a2ef8df
fix: forgot to export quotesrouter and add to the main router
michaelbrusegard Mar 13, 2025
b28893e
feat: add frontend for quotes
michaelbrusegard Mar 13, 2025
a127efa
Merge branch 'dev' into quotes
ZeroWave022 Aug 9, 2025
6998cce
feat: Add quotes to header
ZeroWave022 Aug 9, 2025
175ba1f
fix: Update old code
ZeroWave022 Aug 9, 2025
ca420a4
feat: Add internal quotes, various other UI fixes
ZeroWave022 Aug 9, 2025
a526324
feat: Add ComboboxField
ZeroWave022 Aug 9, 2025
b259c3a
feat: Select user with Combobox, add quote in both locales
ZeroWave022 Aug 9, 2025
f9b5131
feat: Add updating and deleting quotes
ZeroWave022 Aug 9, 2025
84e69ae
feat: Add loading to quotes pages
ZeroWave022 Aug 9, 2025
8a21f2d
chore: Add metadata generation to quotes pages
ZeroWave022 Aug 9, 2025
8390d07
fix: Allow all members to create quotes
ZeroWave022 Sep 29, 2025
178d5dd
fix: "Heard by" translation
ZeroWave022 Sep 29, 2025
d522ef1
fix: Process quote id more thoroughly
ZeroWave022 Sep 29, 2025
107bfda
refactor: Rename get to fetch in quote router
ZeroWave022 Sep 29, 2025
cc3e6a6
fix: Extra check for perms only for internal quotes
ZeroWave022 Sep 29, 2025
787ee20
chore: Remove unused schema
ZeroWave022 Sep 29, 2025
ce25531
refactor: Rename quote relations
ZeroWave022 Sep 29, 2025
6cb9343
Merge branch 'dev' into quotes
ZeroWave022 Sep 29, 2025
a4f82df
feat: Move quote translations to own table
ZeroWave022 Sep 29, 2025
5a1fd4c
feat: Accept more props for Combobox and ComboboxField
ZeroWave022 Sep 29, 2025
d30382e
feat: Improve user search when creating quotes
ZeroWave022 Sep 29, 2025
9cda471
feat: Add pagination to quotes
ZeroWave022 Sep 29, 2025
33b063f
refactor: Provide initial quote user instead of fetching on client
ZeroWave022 Sep 29, 2025
c97888b
fix: Use correct initialts in QuoteCard
ZeroWave022 Sep 29, 2025
2974628
fix: Show more users when searching in QuoteForm
ZeroWave022 Oct 5, 2025
ee7d24d
Merge branch 'dev' into quotes
ZeroWave022 Oct 5, 2025
a631e75
chore: Add DB migration for quotes
ZeroWave022 Oct 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions messages/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"cancel": "Cancel",
"save": "Save",
"uploadText": "Drop files here or click to upload",
"noValidFiles": "No valid files were dropped"
"noValidFiles": "No valid files were dropped",
"noChoicesFound": "No choices found"
},
"error": {
"notFound": "404 - Page not found",
Expand Down Expand Up @@ -120,6 +121,7 @@
"about": "About",
"shiftSchedule": "Shift Schedule",
"members": "Members",
"quotes": "Quotes",
"desktopNavMenu": "{open, select, true {Open} other {Close}} navigation menu",
"goToMatrix": "Go to Matrix",
"rules": "Rules",
Expand Down Expand Up @@ -920,13 +922,6 @@
"description": "Manage banners displayed on the website."
}
},
"api": {
"tooManyRequests": "Too many requests, please try again later",
"notAuthenticated": "Not authenticated",
"notAuthorized": "Not authorized",
"noFileFound": "No file found",
"unableToUpdateMatrix": "Unable to update on Matrix"
},
"privacy": {
"title": "Privacy Policy",
"lastUpdated": "Last updated: 9th July 2025",
Expand Down Expand Up @@ -990,6 +985,69 @@
"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."
}
},
"quotes": {
"title": "Inspirational Quotes",
"backToQuotes": "Back to quotes",
"heardByUser": "Heard by: {user}",
"new": {
"title": "New Quote",
"createQuote": "Create quote",
"success": "Quote created",
"unauthorized": "Unauthorized to create new quotes"
},
"update": {
"title": "Update Quote",
"updateQuote": "Update quote",
"success": "Quote updated",
"unauthorized": "Unauthorized to update quotes"
},
"delete": {
"title": "Are you sure you want to delete this quote?",
"description": "This action cannot be undone.",
"success": "Quote deleted successfully",
"unauthorized": "Unauthorized to delete quotes"
},
"form": {
"userId": {
"label": "User",
"required": "User is required",
"placeholder": "Search for a user...",
"description": "Select a user"
},
"content": {
"labelEnglish": "Quote (English)",
"labelNorwegian": "Quote (Norwegian)",
"placeholderEnglish": "My heart cries because I do not own a 3D printer",
"placeholderNorwegian": "Mitt hjerte gråter fordi jeg ikke eier en 3D-printer",
"required": "Quote is required",
"minLength": "Quote must be at least {count} characters"
},
"internal": {
"label": "Internal quote",
"description": "If selected, this quote will only be visible to Hackerspace members."
},
"loadingMembers": "Loading members...",
"noMembersFound": "No members found"
},
"api": {
"invalidId": "Invalid ID",
"invalidOffset": "Invalid offset",
"insertFailed": "Failed to add quote",
"updateFailed": "Failed to update quote",
"deleteFailed": "Failed to delete quote",
"failedToFetchQuotes": "The server failed to get quotes",
"noUserWithUsername": "No user with provided username found",
"quoteNotFound": "Quote not found"
}
},
"api": {
"internalServerError": "Internal server error",
"tooManyRequests": "Too many requests, please try again later",
"notAuthenticated": "Not authenticated",
"notAuthorized": "Not authorized",
"noFileFound": "No file found",
"unableToUpdateMatrix": "Unable to update on Matrix"
},
"reservations": {
"title": "Reservations",
"backButton": "Back",
Expand Down
74 changes: 66 additions & 8 deletions messages/nb-NO.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"cancel": "Avbryt",
"save": "Lagre",
"uploadText": "Slipp filer her eller klikk for å laste opp",
"noValidFiles": "Ingen gyldige filer ble sluppet"
"noValidFiles": "Ingen gyldige filer ble sluppet",
"noChoicesFound": "Ingen valg funnet"
},
"error": {
"notFound": "404 - Siden ble ikke funnet",
Expand Down Expand Up @@ -120,6 +121,7 @@
"about": "Om oss",
"shiftSchedule": "Vaktliste",
"members": "Medlemmer",
"quotes": "Sitater",
"desktopNavMenu": "{open, select, true {Åpne} other {Lukk}} navigasjonsmeny",
"goToMatrix": "Dra til Matrix",
"rules": "Regler",
Expand Down Expand Up @@ -920,13 +922,6 @@
"description": "Administrer bannere som vises på nettsiden."
}
},
"api": {
"tooManyRequests": "For mange forespørsler. Vennligst vent noen minutter og prøv igjen",
"notAuthenticated": "Ikke autentisert",
"notAuthorized": "Ikke autorisert",
"noFileFound": "Ingen fil funnet",
"unableToUpdateMatrix": "Kunne ikke oppdatere på Matrix"
},
"privacy": {
"title": "Personvernerklæring",
"lastUpdated": "Sist oppdatert: 9. juli 2025",
Expand Down Expand Up @@ -990,6 +985,69 @@
"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."
}
},
"quotes": {
"title": "Inspirerende sitater",
"backToQuotes": "Tilbake til sitater",
"heardByUser": "Hørt av: {user}",
"new": {
"title": "Nytt sitat",
"createQuote": "Opprett sitat",
"success": "Sitat opprettet",
"unauthorized": "Du har ikke rettigheter til å opprette nye sitater"
},
"update": {
"title": "Oppdater sitat",
"updateQuote": "Oppdater sitat",
"success": "Sitat oppdatert",
"unauthorized": "Du har ikke rettigheter til å oppdatere sitater"
},
"delete": {
"title": "Er du sikker på at du vil slette dette sitatet?",
"description": "Denne handlingen kan ikke angres.",
"success": "Sitat slettet",
"unauthorized": "Du har ikke rettigheter til å slette sitater"
},
"form": {
"userId": {
"label": "Bruker",
"required": "Bruker er påkrevd",
"placeholder": "Søk etter en bruker...",
"description": "Velg en bruker"
},
"content": {
"labelEnglish": "Sitat (engelsk)",
"labelNorwegian": "Sitat (norsk)",
"placeholderEnglish": "My heart cries because I do not own a 3D printer",
"placeholderNorwegian": "Mitt hjerte gråter fordi jeg ikke eier en 3D-printer",
"required": "Sitat er påkrevd",
"minLength": "Sitat må være minst {count} tegn"
},
"internal": {
"label": "Internt sitat",
"description": "Hvis valgt, vil dette sitatet kun være synlig for Hackerspace-medlemmer."
},
"loadingMembers": "Laster inn medlemmer...",
"noMembersFound": "Ingen medlemmer funnet"
},
"api": {
"invalidId": "Ugyldig ID",
"invalidOffset": "Ugyldig offset",
"insertFailed": "Kunne ikke opprette sitat",
"updateFailed": "Kunne ikke oppdatere sitat",
"deleteFailed": "Kunne ikke slette sitat",
"failedToFetchQuotes": "Serveren klarte ikke å hente sitater",
"noUserWithUsername": "Ingen bruker med oppgitt brukernavn funnet",
"quoteNotFound": "Sitat ikke funnet"
}
},
"api": {
"internalServerError": "Intern serverfeil",
"tooManyRequests": "For mange forespørsler. Vennligst vent noen minutter og prøv igjen",
"notAuthenticated": "Ikke autentisert",
"notAuthorized": "Ikke autorisert",
"noFileFound": "Ingen fil funnet",
"unableToUpdateMatrix": "Kunne ikke oppdatere på Matrix"
},
"reservations": {
"title": "Reservasjoner",
"backButton": "Tilbake",
Expand Down
2 changes: 1 addition & 1 deletion src/app/[locale]/(default)/about/group/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default async function GroupPage({
['labops', 'leadership', 'admin'].includes(g),
) && (
<Link
className='absolute top-0 right-0'
className='-translate-y-1/2 absolute top-1/2 right-0'
href={{
pathname: '/about/group/[name]/edit',
params: { name: group.identifier },
Expand Down
25 changes: 25 additions & 0 deletions src/app/[locale]/(default)/quotes/[quoteId]/edit/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ArrowLeftIcon } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { QuoteFormSkeleton } from '@/components/quotes/QuoteFormSkeleton';
import { Link } from '@/components/ui/Link';

export default async function NewQuoteLoading() {
const t = await getTranslations('quotes');
const tUpdate = await getTranslations('quotes.update');

return (
<>
<Link
className='flex w-fit items-center gap-2'
href='/quotes'
variant='ghost'
size='default'
>
<ArrowLeftIcon />
<span>{t('backToQuotes')}</span>
</Link>
<h1 className='text-center'>{tUpdate('title')}</h1>
<QuoteFormSkeleton />
</>
);
}
88 changes: 88 additions & 0 deletions src/app/[locale]/(default)/quotes/[quoteId]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ArrowLeftIcon } from 'lucide-react';
import { notFound } from 'next/navigation';
import { type Locale, type Messages, NextIntlClientProvider } from 'next-intl';
import {
getMessages,
getTranslations,
setRequestLocale,
} from 'next-intl/server';
import { QuoteForm } from '@/components/quotes/QuoteForm';
import { Link } from '@/components/ui/Link';
import { api } from '@/lib/api/server';

export async function generateMetadata() {
const t = await getTranslations('quotes.update');

return {
title: t('title'),
};
}

export default async function NewQuotePage({
params,
}: {
params: Promise<{ locale: Locale; quoteId: string }>;
}) {
const { locale, quoteId } = await params;
setRequestLocale(locale);

const { user } = await api.auth.state();
const t = await getTranslations('quotes');
const tUpdate = await getTranslations('quotes.update');

const processedQuoteId = Number(quoteId);
if (
Number.isNaN(processedQuoteId) ||
!Number.isInteger(processedQuoteId) ||
processedQuoteId < 1
)
return notFound();

const quote = await api.quotes.fetchQuote(processedQuoteId);

if (!quote) return notFound();

if (
!user?.groups.some((g) => ['labops', 'leadership', 'admin'].includes(g)) &&
quote.saidBy.id !== user?.id &&
quote.heardBy.id !== user?.id
) {
// TODO: Actually return a HTTP 401 Unauthorized reponse whenever `unauthorized.tsx` is stable
throw new Error(tUpdate('unauthorized'));
}

const { quotes, ui } = await getMessages();

const quoteUser = await api.users.fetchUser({ id: quote.saidBy.id });

return (
<>
<Link
className='flex w-fit items-center gap-2'
href='/quotes'
variant='ghost'
size='default'
>
<ArrowLeftIcon />
<span>{t('backToQuotes')}</span>
</Link>
<h1 className='text-center'>{tUpdate('title')}</h1>
<NextIntlClientProvider
messages={{ quotes, ui } as Pick<Messages, 'quotes' | 'ui'>}
>
<QuoteForm
quote={quote}
initialUser={
quoteUser && {
id: quoteUser.id as number,
profilePictureUrl: quoteUser.profilePictureUrl,
firstName: quoteUser.firstName as string,
lastName: quoteUser.lastName as string,
profilePictureId: quoteUser.profilePictureId as number,
}
}
/>
</NextIntlClientProvider>
</>
);
}
25 changes: 25 additions & 0 deletions src/app/[locale]/(default)/quotes/new/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ArrowLeftIcon } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { QuoteFormSkeleton } from '@/components/quotes/QuoteFormSkeleton';
import { Link } from '@/components/ui/Link';

export default async function NewQuotePage() {
const t = await getTranslations('quotes');
const tNew = await getTranslations('quotes.new');

return (
<>
<Link
className='flex w-fit items-center gap-2'
href='/quotes'
variant='ghost'
size='default'
>
<ArrowLeftIcon />
<span>{t('backToQuotes')}</span>
</Link>
<h1 className='text-center'>{tNew('title')}</h1>
<QuoteFormSkeleton />
</>
);
}
Loading
Loading