From b7b2b768c5194d172db74f380e0fe1af40c58228 Mon Sep 17 00:00:00 2001 From: ketphan02 Date: Thu, 9 May 2024 21:25:00 -0700 Subject: [PATCH 01/18] Apply type changes and minor syntax changes --- .env.example | 2 +- next.config.js | 3 --- src/_temp_types/api/teams.ts | 1 + src/_temp_types/projects.ts | 19 +++++++++++++++---- .../course/[courseId]/project-sets/page.tsx | 2 +- src/components/SearchBar/index.tsx | 2 +- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index ab84c30..04fe2ef 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -BACKEND_BASE_URI=http://localhost:8000 \ No newline at end of file +NEXT_PUBLIC_BACKEND_URL=http://localhost:8000 diff --git a/next.config.js b/next.config.js index 748d8a7..5aae5da 100644 --- a/next.config.js +++ b/next.config.js @@ -14,9 +14,6 @@ const nextConfig = { return config }, - env: { - BACKEND_BASE_URI: process.env.BACKEND_BASE_URI, - }, } module.exports = nextConfig diff --git a/src/_temp_types/api/teams.ts b/src/_temp_types/api/teams.ts index 3fdb5c9..882539c 100644 --- a/src/_temp_types/api/teams.ts +++ b/src/_temp_types/api/teams.ts @@ -13,6 +13,7 @@ export type ApiTeamTemplate = { updatedAt: string; slug: string; requirements: ApiProjectRequirement[]; + number_of_teams: number; max_people: number; min_people: number; }; diff --git a/src/_temp_types/projects.ts b/src/_temp_types/projects.ts index bb942ac..e5619bd 100644 --- a/src/_temp_types/projects.ts +++ b/src/_temp_types/projects.ts @@ -9,9 +9,14 @@ export type ProjectRequirement = { }; export enum RequirementOperator { - EXACTLY = "exactly", - LESS_THAN = "less than", - MORE_THAN = "more than", + GT = "Greater Than", + GTE = "Greater Than or Equal", + LT = "Less Than", + LTE = "Less Than or Equal", + IN = "In", + NOT_IN = "Not In", + CONTAINS = "Contains", + EQ = "Equal", } /** @@ -19,8 +24,14 @@ export enum RequirementOperator { */ export type Project = { id: number; - name?: string; + name: string; // Specifies the number of teams that can work on this project numberOfTeams: number; requirements?: ProjectRequirement[]; }; + +export type ProjectSet = { + id: number; + name: string; + numProjects: number; +}; diff --git a/src/app/(app)/course/[courseId]/project-sets/page.tsx b/src/app/(app)/course/[courseId]/project-sets/page.tsx index b2cee1d..2ee498c 100644 --- a/src/app/(app)/course/[courseId]/project-sets/page.tsx +++ b/src/app/(app)/course/[courseId]/project-sets/page.tsx @@ -7,7 +7,7 @@ import { redirect } from "next/navigation" import { columns } from "./columns" async function getProjectSetsData(): Promise { - const response = await fetch(process.env.BACKEND_URL + "/api/v1/teamset-templates",) + const response = await fetch(process.env.NEXT_PUBLIC_BACKEND_URL + "/api/v1/teamset-templates",) if (!response.ok) { throw new Error("Unable to fetch project sets from API.") } diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index a6041ab..7f0b8e6 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -12,7 +12,7 @@ const SearchBar = React.forwardRef(({ className,
Date: Thu, 9 May 2024 21:32:31 -0700 Subject: [PATCH 02/18] Add package and adjust data table --- package.json | 1 + src/components/ui/data-table.tsx | 13 ++- src/components/ui/toast.tsx | 129 ++++++++++++++++++++ src/components/ui/toaster.tsx | 35 ++++++ src/components/ui/use-toast.ts | 194 +++++++++++++++++++++++++++++++ yarn.lock | 50 ++++---- 6 files changed, 389 insertions(+), 33 deletions(-) create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/use-toast.ts diff --git a/package.json b/package.json index dfd0c37..ca4c68e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@svgr/webpack": "^8.1.0", "@tanstack/react-table": "^8.11.8", "class-variance-authority": "^0.7.0", diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index 32e2c48..1cd35f4 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -34,13 +34,15 @@ type DataTableSearchBarProps = { type DataTableProps = { columns: ColumnDef[]; data: TData[]; - searchBarOptions: DataTableSearchBarProps; + searchBarOptions?: DataTableSearchBarProps; // Items controlling the action in the table (located in the top right corner of the table) actionItems?: (table: TableType) => React.ReactNode; // Buttons group for bulk actions bulkActionItems?: (selectedRowModels: RowModel) => React.ReactNode; // Function Controlling the action when a row is clicked rowAction?: (row: TData) => void; + // Toggle pagination + isPaginated?: boolean }; const DataTable = ({ @@ -50,6 +52,7 @@ const DataTable = ({ bulkActionItems, actionItems, rowAction, + isPaginated, }: DataTableProps) => { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([],) @@ -77,7 +80,7 @@ const DataTable = ({
{/* make the search bar work on multiple columns of table: firstName, lastName, id */} - ({ .getColumn(searchBarOptions.searchColumn) ?.setFilterValue(event.target.value) }} - /> + />}
{!!bulkActionItems && bulkActionItems(table.getRowModel())} @@ -144,9 +147,9 @@ const DataTable = ({
-
+ {isPaginated &&
} /> -
+
} ) } diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..521b94b --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..e223385 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts new file mode 100644 index 0000000..02e111d --- /dev/null +++ b/src/components/ui/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/yarn.lock b/yarn.lock index 89a72c9..b6458db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2071,6 +2071,25 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-toast@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6" + integrity sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.3" + "@radix-ui/react-use-callback-ref@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90" @@ -6665,16 +6684,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6734,14 +6744,7 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7282,7 +7285,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7300,15 +7303,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From ac82ab38a77a12c3ed1a704c2fdb82422998990d Mon Sep 17 00:00:00 2001 From: ketphan02 Date: Thu, 9 May 2024 21:36:00 -0700 Subject: [PATCH 03/18] Add toast to layout --- src/app/layout.tsx | 2 + src/components/ui/data-table.tsx | 1 + src/components/ui/toast.tsx | 24 +++------ src/components/ui/use-toast.ts | 84 ++++++++++++++++---------------- 4 files changed, 52 insertions(+), 59 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1cdfb90..909bb6a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css" import Navbar from "@/components/Navbar" import Footer from "@/components/Footer" import { Separator } from "@/components/ui/separator" +import {Toaster} from "@/components/ui/toaster" const manrope = Manrope({ subsets: ["latin"] }) @@ -26,6 +27,7 @@ export default function RootLayout({ {children}