Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions messages/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@
"lastUpdated": "Last updated: {date}",
"newArticlesUnauthorized": "Unauthorized to create new articles",
"updateArticlesUnauthorized": "Unauthorized to update articles",
"internalUnauthorized": "Unauthorized to view internal articles",
"form": {
"createArticle": "Create article",
"updateArticle": "Update article",
Expand Down Expand Up @@ -411,6 +412,7 @@
"invalidLimit": "Invalid limit",
"invalidOffset": "Invalid offset",
"tooManyArticles": "Too many articles requested. Cannot fetch more than {count} articles.",
"fetchFailed": "Failed to fetch news article",
"insertFailed": "Failed to add news article",
"articleNotFound": "Article not found",
"articleImageNotFound": "Article image not found"
Expand Down
2 changes: 2 additions & 0 deletions messages/nb-NO.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@
"lastUpdated": "Sist oppdatert: {date}",
"newArticlesUnauthorized": "Du har ikke rettigheter til å opprette nye artikler",
"updateArticlesUnauthorized": "Du har ikke rettigheter til å oppdatere artikler",
"internalUnauthorized": "Du har ikke rettigheter til å se interne artikler",
"form": {
"createArticle": "Opprett artikkel",
"updateArticle": "Oppdater artikkel",
Expand Down Expand Up @@ -411,6 +412,7 @@
"invalidLimit": "Ugyldig limit",
"invalidOffset": "Ugyldig offset",
"tooManyArticles": "For mange artikler ble forespurt. Kan ikke hente mer enn {count} artikler.",
"fetchFailed": "Kunne ikke hente nyhetsartikkel",
"insertFailed": "Kunne ikke legge til nyhetsartikkel",
"articleNotFound": "Artikkel ikke funnet",
"articleImageNotFound": "Artikkelbilde ikke funnet"
Expand Down
71 changes: 56 additions & 15 deletions src/app/[locale]/(default)/events/[eventId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, eq } from 'drizzle-orm';
import { TRPCError } from '@trpc/server';
import {
ArrowLeftIcon,
BookImageIcon,
Expand All @@ -17,15 +17,15 @@ import {
} from 'next-intl/server';
import { ParticipantsTable } from '@/components/events/ParticipantsTable';
import { SignUpButton } from '@/components/events/SignUpButton';
import { ErrorPageContent } from '@/components/layout/ErrorPageContent';
import { SkillIcon } from '@/components/skills/SkillIcon';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar';
import { Badge } from '@/components/ui/Badge';
import { ExternalLink, Link } from '@/components/ui/Link';
import { PlateEditorView } from '@/components/ui/plate/PlateEditorView';
import { Separator } from '@/components/ui/Separator';
import { api } from '@/lib/api/server';
import { db } from '@/server/db';
import { eventLocalizations } from '@/server/db/tables';
import type { RouterOutput } from '@/server/api';

export async function generateMetadata({
params,
Expand All @@ -34,15 +34,29 @@ export async function generateMetadata({
}) {
const { eventId } = await params;
const locale = await getLocale();
if (Number.isNaN(Number(eventId))) return;
const localization = await db.query.eventLocalizations.findFirst({
where: and(
eq(eventLocalizations.eventId, Number(eventId)),
eq(eventLocalizations.locale, locale),
),
});
const processedEventId = Number(eventId);

if (!localization?.name) return;
if (
!eventId ||
Number.isNaN(processedEventId) ||
!Number.isInteger(processedEventId)
) {
return notFound();
}

let event: RouterOutput['events']['fetchEvent'] | null = null;

try {
event = await api.events.fetchEvent(processedEventId);
} catch {
return;
}

const localization = event?.localizations.find(
(localization) => localization.locale === locale,
);

if (!event || !localization) return;

return {
title: `${localization.name}`,
Expand All @@ -65,9 +79,37 @@ export default async function EventDetailsPage({
const t = await getTranslations('events');
const tLayout = await getTranslations('layout');
const { ui, events } = await getMessages();
if (Number.isNaN(Number(eventId))) return notFound();
const processedEventId = Number(eventId);

if (
!eventId ||
Number.isNaN(processedEventId) ||
!Number.isInteger(processedEventId)
) {
return notFound();
}

const event = await api.events.fetchEvent(Number(eventId));
let event: RouterOutput['events']['fetchEvent'] | null = null;

try {
event = await api.events.fetchEvent(processedEventId);
} catch (error) {
console.error(error);
if (
error instanceof TRPCError &&
['INTERNAL_SERVER_ERROR', 'FORBIDDEN'].includes(error.code)
) {
return (
<ErrorPageContent
message={
error.code === 'FORBIDDEN'
? t('api.unauthorized')
: t('api.fetchEventFailed')
}
/>
);
}
}

const localization = event?.localizations.find(
(localization) => localization.locale === locale,
Expand All @@ -78,12 +120,11 @@ export default async function EventDetailsPage({
const { user } = await api.auth.state();

const signedUp = user ? await api.events.isSignedUpToEvent(event.id) : false;

const canEdit = user?.groups.some((group) =>
['labops', 'leadership', 'admin'].includes(group),
);
const participants = canEdit
? await api.events.fetchEventParticipants(Number(eventId))
? await api.events.fetchEventParticipants(processedEventId)
: [];

const imageUrl = event.imageId
Expand Down
61 changes: 53 additions & 8 deletions src/app/[locale]/(default)/news/[articleId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TRPCError } from '@trpc/server';
import { ArrowLeftIcon, SquarePenIcon } from 'lucide-react';
import Image from 'next/image';
import { notFound } from 'next/navigation';
Expand All @@ -9,12 +10,14 @@ import {
} from 'next-intl/server';
import readingTime from 'reading-time';
import { HackerspaceLogo } from '@/components/assets/logos';
import { ErrorPageContent } from '@/components/layout/ErrorPageContent';
import { MemberAvatar } from '@/components/members/MemberAvatar';
import { Avatar, AvatarFallback } from '@/components/ui/Avatar';
import { Badge } from '@/components/ui/Badge';
import { Link } from '@/components/ui/Link';
import { PlateEditorView } from '@/components/ui/plate/PlateEditorView';
import { api } from '@/lib/api/server';
import type { RouterOutput } from '@/server/api';
import { getFileUrl } from '@/server/services/files';

export async function generateMetadata({
Expand All @@ -23,10 +26,18 @@ export async function generateMetadata({
params: Promise<{ locale: string; articleId: string }>;
}) {
const { articleId } = await params;
const article = await api.news.fetchArticle({ id: Number(articleId) });

let article: RouterOutput['news']['fetchArticle'] | null = null;
try {
article = await api.news.fetchArticle({ id: Number(articleId) });
} catch {
return;
}

if (!article) return;

return {
title: article?.localization?.title,
title: article.localization.title ?? '',
};
}

Expand All @@ -38,14 +49,43 @@ export default async function ArticlePage({
const { locale, articleId } = await params;
setRequestLocale(locale as Locale);

if (Number.isNaN(Number(articleId))) return notFound();
const processedArticleId = Number(articleId);

if (
!articleId ||
Number.isNaN(processedArticleId) ||
!Number.isInteger(processedArticleId)
) {
return notFound();
}

const t = await getTranslations('news');
const tLayout = await getTranslations('layout');
const formatter = await getFormatter();
const article = await api.news.fetchArticle({
id: Number(articleId),
incrementViews: true,
});

let article: RouterOutput['news']['fetchArticle'] | null = null;
try {
article = await api.news.fetchArticle({
id: processedArticleId,
incrementViews: true,
});
} catch (error) {
if (
error instanceof TRPCError &&
['INTERNAL_SERVER_ERROR', 'FORBIDDEN'].includes(error.code)
) {
return (
<ErrorPageContent
message={
error.code === 'FORBIDDEN'
? t('internalUnauthorized')
: t('api.fetchFailed')
}
/>
);
}
}

if (!article) {
return notFound();
}
Expand Down Expand Up @@ -139,7 +179,12 @@ export default async function ArticlePage({
</small>
</div>
</div>
<Badge variant='secondary'>{`${article.views} ${t('views')}`}</Badge>
<div className='flex gap-4'>
<Badge variant='secondary'>{`${article.views} ${t('views')}`}</Badge>
{article.internal && (
<Badge className='rounded-full'>{tLayout('internal')}</Badge>
)}
</div>
</section>
<section>
<PlateEditorView value={article.localization.content} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast } from '@/components/ui/Toaster';
import { useDebounceCallback } from '@/lib/hooks/useDebounceCallback';
import { useRouter } from '@/lib/locale/navigation';
import type { RouterOutput, RouterOutputs } from '@/server/api';
import type { RouterOutput } from '@/server/api';

type ToolCalendarProps = {
tool: NonNullable<RouterOutput['tools']['fetchTool']>;
user: RouterOutputs['auth']['state']['user'];
user: RouterOutput['auth']['state']['user'];
};
type CalendarReservation =
RouterOutput['reservations']['fetchCalendarReservations'][number];
Expand Down
4 changes: 2 additions & 2 deletions src/components/shift-schedule/MemberList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getTranslations } from 'next-intl/server';
import { SkillIcon } from '@/components/skills/SkillIcon';
import type { RouterOutputs } from '@/server/api';
import type { RouterOutput } from '@/server/api';

type MemberListProps = {
members: RouterOutputs['shiftSchedule']['fetchShifts'][number]['members'];
members: RouterOutput['shiftSchedule']['fetchShifts'][number]['members'];
};

async function MemberList({ members }: MemberListProps) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/shift-schedule/ScheduleCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/Button';
import { TableCell } from '@/components/ui/Table';
import type { days, timeslots } from '@/lib/constants';
import { cx } from '@/lib/utils';
import type { RouterOutputs } from '@/server/api';
import type { RouterOutput } from '@/server/api';
import type { SelectSkill } from '@/server/db/tables';

type ScheduleCellProps = {
Expand All @@ -18,9 +18,9 @@ type ScheduleCellProps = {
};
day: (typeof days)[number];
timeslot: (typeof timeslots)[number];
members: RouterOutputs['shiftSchedule']['fetchShifts'][number]['members'];
members: RouterOutput['shiftSchedule']['fetchShifts'][number]['members'];
skills: SelectSkill[];
user: RouterOutputs['auth']['state']['user'];
user: RouterOutput['auth']['state']['user'];
};

async function ScheduleCell({
Expand Down
4 changes: 2 additions & 2 deletions src/components/shift-schedule/ScheduleCellDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { MemberList } from '@/components/shift-schedule/MemberList';
import { RegisterShiftForm } from '@/components/shift-schedule/RegisterShiftForm';
import type { days, timeslots } from '@/lib/constants';
import type { RouterOutputs } from '@/server/api';
import type { RouterOutput } from '@/server/api';

type ScheduleCellDialogProps = {
formattedShift: {
Expand All @@ -17,7 +17,7 @@ type ScheduleCellDialogProps = {
};
day: (typeof days)[number];
timeslot: (typeof timeslots)[number];
members: RouterOutputs['shiftSchedule']['fetchShifts'][number]['members'];
members: RouterOutput['shiftSchedule']['fetchShifts'][number]['members'];
memberId: number;
userOnShift: boolean;
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/shift-schedule/ScheduleTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
} from '@/components/ui/Table';
import { api } from '@/lib/api/server';
import { days, timeslots, timeslotTimes } from '@/lib/constants';
import type { RouterOutputs } from '@/server/api';
import type { RouterOutput } from '@/server/api';

type ScheduleTableProps = {
user: RouterOutputs['auth']['state']['user'];
user: RouterOutput['auth']['state']['user'];
};

async function ScheduleTable({ user }: ScheduleTableProps) {
Expand Down
12 changes: 1 addition & 11 deletions src/server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,4 @@ const createCaller = createCallerFactory(router);
type RouterInput = inferRouterInputs<typeof router>;
type RouterOutput = inferRouterOutputs<typeof router>;

type RouterInputs = inferRouterInputs<typeof router>;
type RouterOutputs = inferRouterOutputs<typeof router>;

export {
router,
createCaller,
type RouterInput,
type RouterOutput,
type RouterInputs,
type RouterOutputs,
};
export { router, createCaller, type RouterInput, type RouterOutput };
9 changes: 3 additions & 6 deletions src/server/api/routers/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ const eventsRouter = createRouter({
},
})
.catch((error) => {
console.error(error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: ctx.t('events.api.fetchEventFailed', {
error: error.message,
}),
message: ctx.t('events.api.fetchEventFailed'),
cause: { toast: 'error' },
});
});
Expand All @@ -64,9 +63,7 @@ const eventsRouter = createRouter({
if ((!user || user.groups.length <= 0) && event.internal) {
throw new TRPCError({
code: 'FORBIDDEN',
message: ctx.t('events.api.unauthorized', {
eventId: event.id,
}),
message: ctx.t('events.api.unauthorized'),
cause: { toast: 'error' },
});
}
Expand Down
Loading