Skip to content

Commit

Permalink
refactor/fix: split into components, fix imports and fix item loading…
Browse files Browse the repository at this point in the history
… bug after login (#12) (TT-1779)

* remove unecessary useEffect in UserDetails

* refactor: create context for theme

* refactor: use react import, use dynamic time to refresh from token expiry variable

* refactor: more usage of context for theme

* refactor: split item registration page into multiple components to better follow 'thinking in react'

* fix: wait to load items until user is authenticated to prevent items not being fetched

* Move theme toggler to its own component

* useAuth directly in userdetails component

* Apply suggestion
  • Loading branch information
fredrikmonsen authored Dec 13, 2024
1 parent 9b4b3cb commit 4620af0
Show file tree
Hide file tree
Showing 17 changed files with 435 additions and 330 deletions.
238 changes: 12 additions & 226 deletions src/app/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,15 @@
'use client';

import {Key, useEffect, useState} from 'react';
import {approveItem, deleteLock, getItemImage, getItemMetadata} from '@/services/item.data';
import {useEffect, useState} from 'react';
import {Spinner} from '@nextui-org/spinner';
import NextImage from 'next/image';
import {CalendarDate, DatePicker, Image} from '@nextui-org/react';
import {Controller, SubmitHandler, useForm} from 'react-hook-form';
import {Input} from '@nextui-org/input';
import {Button} from '@nextui-org/button';
import {NewspaperMetadata} from '@/models/NewspaperMetadata';
import {parseDate, today} from '@internationalized/date';
import {useRouter} from 'next/navigation';
import {useAsyncList} from '@react-stately/data';
import {CatalogTitle} from '@/models/CatalogTitle';
import {searchNewspaperTitlesInCatalog} from '@/services/catalog.data';
import {Autocomplete, AutocompleteItem} from '@nextui-org/autocomplete';

interface NewspaperFormInput {
title: string;
titleId: string;
date: CalendarDate;
editionNumber: string;
volume: string;
}
import {MetadataForm} from '@/features/metadata-form';
import {getItemImage, getItemMetadata} from '@/services/item.data';
import {ImageContainer} from '@/components/ui/ImageContainer';

export default function Page({params}: { params: { id: string } }) {
const { register, handleSubmit, control, setValue, formState: {errors}, } = useForm<NewspaperFormInput>({
mode: 'onChange'
});
const [imageSrc, setImageSrc] = useState<string>();
const [extractedMetadata, setExtractedMetadata] = useState<NewspaperMetadata>();
const [loading, setLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();

const onSubmit: SubmitHandler<NewspaperFormInput> = data => {
setIsSubmitting(true);
const metadata: NewspaperMetadata = {
title: data.title,
titleId: data.titleId,
date: data.date.toString().substring(0, 10),
editionNumber: data.editionNumber,
volume: data.volume
};
void approveItem(params.id, metadata).then(res => {
if (res.ok) {
router.push('/');
} else {
throw new Error(`Noe gikk galt ved godkjenning: ${res.status}`);
}
})
.then(async () => {
await handleDeleteLock();
})
.then(() => {
router.push('/');
}).catch(e => {
if (e instanceof Error) {
alert(e.message);
}
});
};

useEffect(() => {
const getItem = async () => {
Expand All @@ -71,188 +20,25 @@ export default function Page({params}: { params: { id: string } }) {
await getItemMetadata(params.id).then(async res => {
const data = await res.json() as NewspaperMetadata;
setExtractedMetadata(data);
setValue('title', data.title);
setValue('titleId', data.titleId);
setLoading(false);
});
};
void getItem();
}, [params.id, setValue]);

const titles = useAsyncList<CatalogTitle>({
async load({signal, filterText}) {
if (!filterText) {
return {items: []};
}
const data = await searchNewspaperTitlesInCatalog(filterText, signal);
return { items: data };
}
});

const dateValue = (date: Date): CalendarDate => {
return parseDate(date.toISOString()?.substring(0, 10));
};

const onSelectionChange = (key: Key | null) => {
const selectedTitle = titles.items.find(title => title.catalogueId === key);
if (selectedTitle) {
setValue('title', selectedTitle.name);
setValue('titleId', selectedTitle.catalogueId);
}
};

const handleDeleteLock = async () => {
await deleteLock(params.id).then(res => {
if (res.ok) {
router.push('/');
} else {
alert('Kunne ikke slette lås.');
}
});
};
}, [params.id]);

return (
<div>
{ loading ?
{ !extractedMetadata ?
<Spinner /> :
<>
{ imageSrc && extractedMetadata &&
<div className="flex items-center">
<div className="image-container">
<Image as={NextImage} layout="fill" className="image" src={imageSrc} alt="Bilde"/>
</div>
<div className="px-2.5 max-w-full">
<div className="min-w-full">
<Autocomplete
label="Finn avistittel"
variant="bordered"
className="min-w-full"
placeholder="Søk etter avistittel"
description="Bruk søkefeltet for å finne tittel og id"
autoFocus
menuTrigger="input"
isLoading={titles.isLoading}
items={titles.items}
onSelectionChange={key => onSelectionChange(key)}
onInputChange={value => titles.setFilterText(value)}
allowsEmptyCollection={false}
allowsCustomValue={true}
>
{ item => (
<AutocompleteItem
key={item.catalogueId}
textValue={item.name}
endContent={
<div className="text-xs text-gray-500">
{item.catalogueId}
</div>
}
>
<div>
{item.name}
<div>
{item.startDate && (
<span className="text-xs text-gray-500">Fra {item.startDate}</span>
)}
{item.endDate && (
<span className="text-xs text-gray-500"> til {item.endDate}</span>
)}
</div>
</div>
</AutocompleteItem>
)}
</Autocomplete>
</div>

<form onSubmit={() => void handleSubmit(onSubmit)()} className="flex flex-col gap-4">
<div className="flex flex-row gap-4">
<Controller
name="title"
control={control}
rules={{required: 'Tittel er påkrevd'}}
render={({ field }) =>
<Input
{...field}
className="w-2/3"
type="text"
label="Tittel"
isDisabled
/>
}
/>
<Controller
name="titleId"
control={control}
rules={{required: 'ID er påkrevd'}}
render={({ field }) =>
<Input
{...field}
className="w-1/3"
type="text"
label="ID"
isDisabled
/>
}
/>
</div>
<Controller
name="date"
control={control}
rules={{
required: 'Dato er påkrevd',
validate: {
futureDate: value => {
return value >= today('Europe/Oslo')
? 'Datoen kan ikke være i fremtiden'
: true;
}
}
}}
defaultValue={dateValue(new Date(extractedMetadata.date))}
render={({field}) => (
<DatePicker
{...field}
value={field.value}
onChange={e => field.onChange(e)}
label="Utgivelsesdato"
variant={'bordered'}
isInvalid={!!errors.date}
errorMessage={errors.date?.message}
/>
)}
/>
<div className="flex flex-row gap-4">
<Input
type="text"
label="Nummer"
variant={'bordered'}
{...register('editionNumber')}
defaultValue={extractedMetadata.editionNumber}
/>
<Input
type="text"
label="Årgang"
variant={'bordered'}
{...register('volume')}
defaultValue={extractedMetadata.volume}
/>
</div>
<Button
color={!isSubmitting ? 'primary' : 'default'} onClick={() => void handleSubmit(onSubmit)()}
isDisabled={isSubmitting}
startContent={isSubmitting && <Spinner className='ml-1' size='sm'/>}
>Godkjenn</Button>
<Button
variant="light"
color="secondary"
onClick={() => void handleDeleteLock()}
>Avbryt</Button>
</form>
{imageSrc &&
<div className="flex items-center">
<ImageContainer src={imageSrc} />
<MetadataForm id={params.id} extractedMetadata={extractedMetadata}/>
</div>
</div>
}
</>
}
</div>
);
)
;
}
16 changes: 10 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type {Metadata} from 'next';
import './globals.css';
import {Providers} from '@/app/providers';
import Header from '@/components/Header';
import Header from '@/components/ui/Header';
import {ReactNode} from 'react';
import {ThemeLayout} from '@/components/layouts/ThemeLayout';
import {ThemeProvider} from '@/providers/ThemeProvider';

export const metadata: Metadata = {
title: 'AMMO',
Expand All @@ -11,11 +14,11 @@ export const metadata: Metadata = {
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: ReactNode;
}>) {
return (
<html lang="en">
<body>
<ThemeProvider>
<ThemeLayout>
<Providers>
<main>
<div className="min-h-screen flex flex-col text-center">
Expand All @@ -29,7 +32,8 @@ export default function RootLayout({
</div>
</main>
</Providers>
</body>
</html>
</ThemeLayout>
</ThemeProvider>

);
}
9 changes: 6 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client';

import ItemThumbnail from '@/components/ItemThumbnail';
import ItemThumbnail from '@/components/ui/ItemThumbnail';
import {Spinner} from '@nextui-org/spinner';
import {useEffect, useState} from 'react';
import {ItemImage} from '@/models/ItemImage';
import {getAllItems, getAllLocks, lockItem} from '@/services/item.data';
import {useAuth} from '@/app/AuthProvider';
import {useAuth} from '@/providers/AuthProvider';
import {useRouter} from 'next/navigation';
import {ItemLock} from '@prisma/client';

Expand Down Expand Up @@ -40,8 +40,11 @@ export default function Home() {
};

useEffect(() => {
if (!user) {
return;
}
void getItems();
}, []);
}, [user]);

const handleItemClicked = async (id: string) => {
if (!user) {
Expand Down
10 changes: 5 additions & 5 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
'use client';

import {AuthProvider} from '@/app/AuthProvider';
import {AuthProvider} from '@/providers/AuthProvider';
import {NextUIProvider} from '@nextui-org/react';
import React from 'react';
import {ReactNode, StrictMode} from 'react';

export function Providers({children}: { children: React.ReactNode }) {
export function Providers({children}: { children: ReactNode }) {
return (
<AuthProvider>
<React.StrictMode>
<StrictMode>
<NextUIProvider locale='nb-NO'>
{children}
</NextUIProvider>
</React.StrictMode>
</StrictMode>
</AuthProvider>
);
}
Loading

0 comments on commit 4620af0

Please sign in to comment.