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
36 changes: 36 additions & 0 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import GroupCard, { GroupCardSkeleton } from '@/components/GroupCard/GroupCard';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { GiftExchangeWithMemberCount } from '../types/giftExchange';
import { useToast } from '@/hooks/use-toast';
import { ToastVariants } from '@/components/Toast/Toast.enum';

export default function Dashboard() {
const [giftExchanges, setGiftExchanges] = useState<
GiftExchangeWithMemberCount[]
>([]);
const [isLoading, setIsLoading] = useState(true);
const { toast } = useToast();

useEffect(() => {
async function fetchGiftExchanges() {
Expand All @@ -28,6 +31,39 @@ export default function Dashboard() {

const data = await response.json();
setGiftExchanges(data);

const today = new Date();
for (const exchange of data) {
const drawingDate = new Date(exchange.drawing_date);
const timeDifference = drawingDate.getTime() - today.getTime();
const dayDifference = Math.ceil(
timeDifference / (1000 * 60 * 60 * 24),
);

if (dayDifference > 0 && dayDifference <= 3) {
toast({
variant: ToastVariants.Warning,
title: `Upcoming Draw - ${exchange.name}`,
description: `The draw is in ${dayDifference} day${dayDifference < 2 ? '' : 's'}!`,
group: exchange.gift_exchange_id,
});
} else if (dayDifference === 0) {
toast({
variant: ToastVariants.Success,
title: `Draw Today - ${exchange.name}`,
description: `Go to your group to initiate the gift exchange draw.`,
group: exchange.gift_exchange_id,
});
} else if (dayDifference < 0) {
toast({
variant: ToastVariants.Error,
title: `Draw date has passed - ${exchange.name}`,
description:
'Your Secret Santas are still secret! Please draw now or reschedule drawing date.',
group: exchange.gift_exchange_id,
});
}
}
} catch (error) {
console.error('Failed to fetch gift exchanges:', error);
} finally {
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import GlobalHeader from '@/components/GlobalHeader/GlobalHeader';
import { SnowOverlayProvider } from '@/providers/SnowOverlayProvider';
import SnowOverlayWrapper from '@/components/SnowOverlayWrapper/SnowOverlayWrapper';
import AuthContextProvider from '@/context/AuthContextProvider';
import Toaster from '@/components/Toaster/Toaster';

const geistSans = localFont({
src: './fonts/GeistVF.woff',
Expand Down Expand Up @@ -48,6 +49,7 @@ const RootLayout = ({
{children}
</SnowOverlayProvider>
</AuthContextProvider>
<Toaster />
</body>
</html>
);
Expand Down
30 changes: 17 additions & 13 deletions components/ToastAction/ToastAction.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { ToastAction } from "./ToastAction";
import { ToastAction } from './ToastAction';
import { render, screen, fireEvent } from '@testing-library/react';

describe('ToastAction', () => {
it('renders the children content within ToastAction', () => {
render(<ToastAction altText="undo">Undo</ToastAction>)
it('renders the children content within ToastAction', () => {
render(<ToastAction altText="undo">Undo</ToastAction>);

const toastAction = screen.getByTestId('toastAction');
expect(toastAction).toHaveTextContent('Undo');
});
const toastAction = screen.getByTestId('toastAction');
expect(toastAction).toHaveTextContent('Undo');
});

it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<ToastAction altText="undo" onClick={handleClick}>Undo</ToastAction>)
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(
<ToastAction altText="undo" onClick={handleClick}>
Undo
</ToastAction>,
);

fireEvent.click(screen.getByText('Undo'));
expect(handleClick).toHaveBeenCalled()
})
})
fireEvent.click(screen.getByText('Undo'));
expect(handleClick).toHaveBeenCalled();
});
});
16 changes: 8 additions & 8 deletions components/ToastAction/ToastAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// Licensed under the MIT License.

import React from 'react';
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cn } from "@/lib/utils"
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cn } from '@/lib/utils';

const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
Expand All @@ -13,14 +13,14 @@ const ToastAction = React.forwardRef<
data-testid="toastAction"
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
));
ToastAction.displayName = ToastPrimitives.Action.displayName;

type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;

export { ToastAction, type ToastActionElement }
export { ToastAction, type ToastActionElement };
27 changes: 19 additions & 8 deletions components/ToastDescription/ToastDescription.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,31 @@ import { ToastDescription } from './ToastDescription';

describe('ToastDescription', () => {
it('renders the description text', () => {
const { getByText } = render(<ToastDescription>Test Description</ToastDescription>);
const { getByText } = render(
<ToastDescription>Test Description</ToastDescription>,
);
expect(getByText('Test Description')).toBeInTheDocument();
});

it('applies custom classNames', () => {
render(<ToastDescription className="custom-class">Test Description</ToastDescription>)
render(
<ToastDescription className="custom-class">
Test Description
</ToastDescription>,
);

const toastDescription = screen.getByTestId('toastDescription');
expect(toastDescription).toHaveClass('custom-class')
})
expect(toastDescription).toHaveClass('custom-class');
});

it('passes custom data attributes', () => {
render(<ToastDescription data-custom-attribute="testValue"></ToastDescription>);
render(
<ToastDescription data-custom-attribute="testValue"></ToastDescription>,
);
const toastDescription = screen.getByTestId('toastDescription');
expect(toastDescription).toHaveAttribute('data-custom-attribute', 'testValue')
})
});
expect(toastDescription).toHaveAttribute(
'data-custom-attribute',
'testValue',
);
});
});
12 changes: 6 additions & 6 deletions components/ToastDescription/ToastDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
// Licensed under the MIT License.

import React from 'react';
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cn } from "@/lib/utils"
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cn } from '@/lib/utils';

const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
className={cn('text-sm opacity-90', className)}
data-testid="toastDescription"
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;

export { ToastDescription }
export { ToastDescription };
8 changes: 4 additions & 4 deletions components/ToastProvider/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) Gridiron Survivor.
// Licensed under the MIT License.

"use client"
'use client';

import * as ToastPrimitives from "@radix-ui/react-toast"
import * as ToastPrimitives from '@radix-ui/react-toast';

const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;

export { ToastProvider }
export { ToastProvider };
10 changes: 5 additions & 5 deletions components/ToastTitle/ToastTitle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ describe('ToastTitle', () => {
render(<ToastTitle className="custom-class"></ToastTitle>);

const toastTitle = screen.getByTestId('toastTitle');
expect(toastTitle).toHaveClass('custom-class')
})
expect(toastTitle).toHaveClass('custom-class');
});

it('passes custom data attributes', () => {
render(<ToastTitle data-custom-attribute="testValue"></ToastTitle>);
const toastTitle = screen.getByTestId('toastTitle');
expect(toastTitle).toHaveAttribute('data-custom-attribute', 'testValue')
})
});
expect(toastTitle).toHaveAttribute('data-custom-attribute', 'testValue');
});
});
14 changes: 7 additions & 7 deletions components/ToastTitle/ToastTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Gridiron Survivor.
// Licensed under the MIT License.

import React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cn } from "@/lib/utils"
import React from 'react';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cn } from '@/lib/utils';

const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
Expand All @@ -12,10 +12,10 @@ const ToastTitle = React.forwardRef<
<ToastPrimitives.Title
data-testid="toastTitle"
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;

export { ToastTitle }
export { ToastTitle };
2 changes: 0 additions & 2 deletions hooks/use-toast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import type { Action } from './use-toast';

import React from 'react';

// methods to test: addToRemoveQueue, dispatch, toast, useToast

const MOCK_TOAST_1 = { id: '1', title: 'Toast 1', open: true };
const MOCK_TOAST_2 = { id: '2', title: 'Toast 2', open: true };
const TOAST_LIMIT = 1;
Expand Down
29 changes: 20 additions & 9 deletions hooks/use-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
'use client';

// Inspired by react-hot-toast library
import * as React from 'react';
import { ReactNode, useState, useEffect } from 'react';

import type { ToastActionElement } from '@/components/ToastAction/ToastAction';
import type { ToastProps } from '@/components/Toast/Toast';

export const TOAST_LIMIT = 1;
export const TOAST_REMOVE_DELAY = 1000000;
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;

type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
title?: ReactNode;
description?: ReactNode;
action?: ToastActionElement;
group?: string;
};

export const actionTypes = {
Expand Down Expand Up @@ -93,6 +94,16 @@ export const addToRemoveQueue = (toastId: string): void => {
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'ADD_TOAST':
const isDuplicate = state.toasts.some(
(toast) =>
toast.title === action.toast.title &&
toast.description === action.toast.description,
);

if (isDuplicate) {
return state;
}

return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
Expand Down Expand Up @@ -122,7 +133,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || !toastId
t.id === toastId || typeof toastId === 'undefined'
? {
...t,
open: false,
Expand All @@ -132,7 +143,7 @@ export const reducer = (state: State, action: Action): State => {
};
}
case 'REMOVE_TOAST':
if (!action.toastId) {
if (typeof action.toastId === 'undefined') {
return {
...state,
toasts: [],
Expand Down Expand Up @@ -230,9 +241,9 @@ export interface UseToastReturn extends State {
* @returns {object} An object containing the current toast state and action functions.
*/
export function useToast(): UseToastReturn {
const [state, setState] = React.useState<State>(memoryState);
const [state, setState] = useState<State>(memoryState);

React.useEffect(() => {
useEffect(() => {
listeners.push(setState);
return (): void => {
const index = listeners.indexOf(setState);
Expand Down
Loading