Skip to content

Commit

Permalink
Merge pull request #62 from samuliasmala/antti/add-tanstack-library-i…
Browse files Browse the repository at this point in the history
…ndex-page

Valmis, mergetään!
  • Loading branch information
anttiasmala authored Jan 22, 2025
2 parents 3d32384 + 7960798 commit 67dd887
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 51 deletions.
20 changes: 10 additions & 10 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@

# Enter-näppäimellä EditModalin ja DeleteModalin hyväksyminen

### </IDEOITA>
# Jos tulee virhe vaikka lahjoja hakiessa, ehkä jokin nappula olisi hyvä tehdä, jolla voisi mahdollisesti kokeilla hakea uudestaan lahjoja

TODO:
# Kun lisää uuden lahjan / kun lahjoja haetaan, järjestetään ne uusin -> vanhin. Myöhemmin voidaan lisätä käyttäjälle mahdollisuus valita miten lahjat haluaa järjestää (vaikka vanhin -> uusin, aakkosjärjestys, ymsyms)

Lisää /pages/login.tsx:n Kirjaudu sisään -nappulaan "select-none" jottei teksti tule maalatuksi jos silmää painaa muutaman kerran liian nopeasti
# Tässä toteutustapa:const sortedArrayNewestToOldest = gifts.sort((a, b) =>new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),);

Jos tulee virhe vaikka lahjoja hakiessa, ehkä jokin nappula olisi hyvä tehdä, jolla voisi mahdollisesti kokeilla hakea uudestaan lahjoja
# Tanstackin requesteihin joku maksimiaika mitä odotetaan

Kun lisää uuden lahjan / kun lahjoja haetaan, järjestetään ne uusin -> vanhin. Myöhemmin voidaan lisätä käyttäjälle mahdollisuus valita miten lahjat haluaa järjestää (vaikka vanhin -> uusin, aakkosjärjestys, ymsyms)
### </IDEOITA>

TODO:

Tässä toteutustapa:
1. Tee https://github.com/samuliasmala/lahjalista/pull/62#discussion_r1921024704 omaan branchiin

const sortedArrayNewestToOldest = gifts.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
2.1 Vaihda "Kirjaudu sisään"- ja "Luo käyttäjätunnus" -nappuloihin pyörivä spinner-indikaattori "..." sijaan
2.2 Lisää /pages/login.tsx:n Kirjaudu sisään -nappulaan "select-none" jottei teksti tule maalatuksi jos silmää painaa muutaman kerran liian nopeasti
16 changes: 10 additions & 6 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { ButtonHTMLAttributes } from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { twMerge } from 'tailwind-merge';
import { errorWrapper } from '~/utils/utilFunctions';

export function Button({
children,
className,
...rest
}: ButtonHTMLAttributes<HTMLButtonElement>) {
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
onClick?: (
event: React.MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
) => void | Promise<void>;
};

export function Button({ children, onClick, className, ...rest }: ButtonProps) {
return (
<button
onClick={onClick === undefined ? undefined : errorWrapper(onClick)}
className={twMerge(
`mt-6 w-full rounded-md border border-lines bg-primary p-2 text-lg font-medium text-white disabled:bg-gray-300 disabled:text-gray-500`,
className,
Expand Down
27 changes: 27 additions & 0 deletions icons/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
MIT License
You are free:
to share – to copy, distribute and transmit the work
to remix – to adapt the work
Under the following terms:
liability – the author doesn't provide any warranty and doesn't accepts any liability
copyright notice – a copy of the license or copyright notice must be included with software
share alike – If you remix, transform, or build upon the material, you can distribute your work under any license
Credits: https://www.svgrepo.com/svg/310005/spinner-ios
*/

import type { SVGProps } from 'react';

const SvgSpinner = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" {...props}>
<path
fill="currentColor"
fillRule="nonzero"
d="M10 3a7 7 0 0 0-7 7 .5.5 0 0 1-1 0 8 8 0 1 1 8 8 .5.5 0 0 1 0-1 7 7 0 1 0 0-14"
/>
</svg>
);
export default SvgSpinner;
108 changes: 76 additions & 32 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import SvgPencilEdit from '~/icons/pencil_edit';
import SvgTrashCan from '~/icons/trash_can';
import axios from 'axios';
import { handleErrorToast } from '~/utils/handleToasts';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import SvgSpinner from '~/icons/spinner';
import { useQueryClient, useMutation } from '@tanstack/react-query';
import { useShowErrorToast } from '~/hooks/useShowErrorToast';

export { getServerSideProps };
Expand All @@ -30,9 +32,15 @@ export default function Home({

const [showUserWindow, setShowUserWindow] = useState(false);

const { isFetching, isError, error } = useGetGifts();

useShowErrorToast(error);
const createGiftQuery = useMutation({
mutationKey: QueryKeys.CREATE_GIFT,
mutationFn: async (newGift: CreateGift) => await createGift(newGift),
// if success: refresh giftlist
onSuccess: async () =>
await queryClient.invalidateQueries({
queryKey: QueryKeys.GIFTS,
}),
});

const queryClient = useQueryClient();

Expand Down Expand Up @@ -61,12 +69,8 @@ export default function Home({
receiver: newReceiver,
gift: newGiftName,
};

await createGift(newGift);
// reloads gift list!
await queryClient.invalidateQueries({
queryKey: QueryKeys.GIFTS,
});
// send gift creation request
await createGiftQuery.mutateAsync(newGift);

setNewGiftName('');
setNewReceiver('');
Expand Down Expand Up @@ -134,10 +138,19 @@ export default function Home({
</div>
<Button
type="submit"
className={`mt-8 ${isFetching || isError ? 'cursor-not-allowed bg-red-500' : null}`}
disabled={isFetching || isError}
className="mt-8"
disabled={createGiftQuery.isPending}
>
Lisää
{createGiftQuery.isPending ? (
<span className="absolute p-1">
<SvgSpinner
width={18}
height={18}
className="animate-spin text-black"
/>
</span>
) : null}
</Button>
</form>
</div>
Expand All @@ -159,7 +172,12 @@ function GiftList() {

if (isFetching)
return (
<p className="loading-dots mt-4 text-lg font-bold">Noudetaan lahjoja</p>
<p className="mt-4 text-lg font-bold">
Noudetaan lahjoja{' '}
<span className="absolute ml-2 mt-1.5">
<SvgSpinner width={18} height={18} className="animate-spin" />
</span>
</p>
);

if (error) return <p className="mt-5 bg-red-500 text-lg">{error.message}</p>;
Expand Down Expand Up @@ -231,22 +249,32 @@ function UserDetailModal({
showUserWindow: boolean;
closeUserWindow: () => void;
}) {
async function handleLogout() {
try {
await axios.post('/api/auth/logout');
window.location.href = '/logout';
} catch (e) {
console.error(e);
window.location.href = '/';
}
}
const router = useRouter();

const queryClient = useQueryClient();

const { isPending, error, mutateAsync } = useMutation({
mutationKey: QueryKeys.LOGOUT,
mutationFn: async () => await axios.post('/api/auth/logout'),
onSuccess: () => {
queryClient.clear();
router.push('/logout').catch((e) => console.error(e));
},
});

useShowErrorToast(error);

if (user && showUserWindow) {
return (
<>
<div
className="fixed left-0 top-0 h-full w-full max-w-full bg-transparent"
onClick={() => closeUserWindow()}
className={`fixed left-0 top-0 h-full w-full max-w-full bg-transparent ${isPending ? 'z-[100]' : ''}`}
onClick={() => {
// this blocks the closing of the User Modal if request for logout is sent
if (!isPending) {
closeUserWindow();
}
}}
/>
<div className="absolute right-1 top-12 z-[99] w-56 rounded-md border-2 border-lines bg-bgForms shadow-md shadow-black">
<p className="overflow mb-0 ml-3 mt-3 font-bold [overflow-wrap:anywhere]">
Expand All @@ -255,15 +283,31 @@ function UserDetailModal({
<p className="ml-3 [overflow-wrap:anywhere]">{user.email}</p>
<div className="flex w-full justify-center">
<Button
className="mb-4 ml-3 mr-3 mt-4 flex h-8 w-full max-w-56 items-center justify-center rounded-md bg-primary"
onClick={() => void handleLogout()}
className="mb-4 ml-3 mr-3 mt-4 flex h-8 w-full max-w-56 items-center justify-center rounded-md bg-primary text-sm font-medium"
onClick={async () => {
try {
await mutateAsync();
} catch (e) {
handleErrorToast(handleError(e));
}
}}
disabled={isPending}
>
<p className={`text-sm font-medium text-white`}>Kirjaudu ulos</p>
<SvgArrowRightStartOnRectangle
width={18}
height={18}
className="ml-2"
/>
{' '}
Kirjaudu ulos
{isPending ? (
<SvgSpinner
width={18}
height={18}
className="ml-2 animate-spin"
/>
) : (
<SvgArrowRightStartOnRectangle
width={18}
height={18}
className="ml-2"
/>
)}
</Button>
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Label } from '~/components/Label';
import { GetServerSidePropsContext } from 'next';
import { handleErrorToast } from '~/utils/handleToasts';
import { ErrorParagraph } from '~/components/ErrorParagraph';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useShowErrorToast } from '~/hooks/useShowErrorToast';

export async function getServerSideProps(context: GetServerSidePropsContext) {
Expand Down Expand Up @@ -48,11 +48,16 @@ export default function Login() {

const router = useRouter();

const queryClient = useQueryClient();

const { mutateAsync, isPending, error } = useMutation({
mutationKey: QueryKeys.LOGIN,
mutationFn: async (loginCredentials: UserLoginDetails) =>
await axios.post('/api/auth/login', loginCredentials),
onSuccess: () => router.push('/'),
onSuccess: () => {
queryClient.clear();
router.push('/').catch((e) => console.error(e));
},
});

useShowErrorToast(error);
Expand Down
3 changes: 2 additions & 1 deletion public/credits_for_images.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ eye_open.svg = https://heroicons.com/
eye_slash.svg = https://heroicons.com/
user.svg = https://heroicons.com/
arrow-right-start-on-rectangle.svg = https://heroicons.com/
logo = https://www.freepik.com/free-vector/flat-wheat-collection_1545231.htm
logo = https://www.freepik.com/free-vector/flat-wheat-collection_1545231.htm
spinner.svg = https://www.svgrepo.com/svg/310005/spinner-ios
27 changes: 27 additions & 0 deletions public/images/icons/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export const QueryKeys = {
GIFTS: ['gifts'],
LOGIN: ['login'],
REGISTER: ['register'],
CREATE_GIFT: ['createGift'],
LOGOUT: ['logout'],
// ei vielä käytössä
//GIFT_DETAILS: (uuid: string) => ['gifts', uuid],
};
35 changes: 35 additions & 0 deletions utils/utilFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,38 @@ export async function sleep(timeoutTimeInMs: number) {
export function randomBoolean() {
return !Math.round(Math.random());
}

/**
* A higher-order function that wraps a given function to handle both
* synchronous and asynchronous errors. Used to ensure that errors are
* caught in event handlers and other places where they would otherwise be
* uncaugh.
*
* @template A - The type of the arguments that the wrapped function accepts.
* @param {(...args: A) => Promise<void> | void} fn - The function to be
* wrapped. It can be either synchronous or asynchronous.
* @returns {(...args: A) => void} - A new function that wraps the original
* function and handles errors and returns a synchronous result.
*
* @example
* // Usage with an asynchronous function in click handler
* const handleClick = async () => { throw new Error('Asynchronous error'); };
*
* <Button onClick={errorWrapper(handleClick)}>Click me</Button>
*/
export function errorWrapper<A extends unknown[]>(
fn: (...args: A) => Promise<void> | void,
): (...args: A) => void {
return (...args: A) => {
try {
const p = fn(...args);
// Check if the function returns a promise to catch async errors
if (p instanceof Promise)
p.catch((error: unknown) => {
console.error('Error thrown asynchronously', error);
});
} catch (error) {
console.error('Error thrown synchronously', error);
}
};
}

0 comments on commit 67dd887

Please sign in to comment.