feat: monorepo

This commit is contained in:
Henry
2024-12-12 14:34:10 +00:00
parent b8eed7a90c
commit 0c8d17dce5
370 changed files with 10280 additions and 39805 deletions

View File

@@ -0,0 +1,210 @@
import type { ReactNode } from "react";
import React, { createContext, useContext, useState } from "react";
import {
type GetBoardByIdOutput,
type NewCardInput,
type NewListInput,
type ReorderCardInput,
type ReorderListInput,
} from "@kan/api/types";
import { generateUID } from "@kan/utils";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
interface BoardContextProps {
boardData: GetBoardByIdOutput;
setBoardData: React.Dispatch<React.SetStateAction<GetBoardByIdOutput>>;
updateList: (params: ReorderListInput) => void;
updateCard: (params: ReorderCardInput) => void;
addCard: (params: NewCardInput) => void;
addList: (params: NewListInput) => void;
removeCard: (params: { cardPublicId: string }) => void;
refetchBoard: () => Promise<void>;
}
const initialBoardData: GetBoardByIdOutput = {
name: "",
publicId: "",
lists: [],
labels: [],
workspace: {
publicId: "",
members: [],
},
};
const BoardContext = createContext<BoardContextProps | undefined>(undefined);
export const BoardProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const utils = api.useUtils();
const [boardData, setBoardData] =
useState<GetBoardByIdOutput>(initialBoardData);
const { showPopup } = usePopup();
const refetchBoard = async () => {
if (!boardData?.publicId) return;
try {
await utils.board.byId.refetch();
} catch (e) {
showPopup({
header: "Error fetching board",
message: "Please try again later, or contact customer support.",
});
}
};
const updateCardMutation = api.card.reorder.useMutation({
onSuccess: async () => {
await refetchBoard();
},
onError: async () => {
await refetchBoard();
showPopup({
header: "Unable to update card",
message: "Please try again later, or contact customer support.",
});
},
});
const updateListMutation = api.list.reorder.useMutation({
onSuccess: async () => {
await refetchBoard();
},
onError: async () => {
await refetchBoard();
showPopup({
header: "Unable to update list",
message: "Please try again later, or contact customer support.",
});
},
});
const addCard = ({
title,
listPublicId,
labelPublicIds,
memberPublicIds,
position,
}: {
title: string;
listPublicId: string;
labelPublicIds: string[];
memberPublicIds: string[];
position: "start" | "end";
}) => {
if (!boardData) return;
const updatedLists = boardData.lists.map((list) => {
if (list.publicId === listPublicId) {
const newCard = {
publicId: `PLACEHOLDER_${generateUID()}`,
title,
listId: 2,
description: "",
labels: boardData.labels.filter((label) =>
labelPublicIds.includes(label.publicId),
),
members:
boardData.workspace?.members.filter((member) =>
memberPublicIds.includes(member.publicId),
) ?? [],
index: position === "start" ? 0 : list.cards.length,
};
const updatedCards =
position === "start"
? [newCard, ...list.cards]
: [...list.cards, newCard];
return { ...list, cards: updatedCards };
}
return list;
});
setBoardData({ ...boardData, lists: updatedLists });
};
const addList = ({ name, boardPublicId }: NewListInput) => {
if (!boardData) return;
const newList = {
publicId: generateUID(),
name,
boardId: 1,
boardPublicId,
cards: [],
index: boardData.lists.length,
};
const updatedLists = [...boardData.lists, newList];
setBoardData({ ...boardData, lists: updatedLists });
};
const removeCard = ({ cardPublicId }: { cardPublicId: string }) => {
if (!boardData) return;
const updatedLists = boardData.lists.map((list) => {
const updatedCards = list.cards.filter(
(card) => card.publicId !== cardPublicId,
);
return { ...list, cards: updatedCards };
});
setBoardData({ ...boardData, lists: updatedLists });
};
const updateList = ({
listPublicId,
currentIndex,
newIndex,
}: ReorderListInput) => {
updateListMutation.mutate({
listPublicId,
currentIndex,
newIndex,
});
};
const updateCard = ({
cardPublicId,
newListPublicId,
newIndex,
}: ReorderCardInput) => {
updateCardMutation.mutate({
cardPublicId,
newListPublicId,
newIndex,
});
};
return (
<BoardContext.Provider
value={{
boardData,
setBoardData,
updateList,
updateCard,
addCard,
addList,
removeCard,
refetchBoard,
}}
>
{children}
</BoardContext.Provider>
);
};
export const useBoard = () => {
const context = useContext(BoardContext);
if (context === undefined) {
throw new Error("useBoard must be used within a BoardProvider");
}
return context;
};

View File

@@ -0,0 +1,65 @@
import { createContext, useContext, useState } from "react";
type ModalContextType = {
isOpen: boolean;
openModal: (
contentType: string,
entityId?: string,
entityLabel?: string,
) => void;
closeModal: () => void;
modalContentType: string;
entityId: string;
entityLabel: string;
};
interface Props {
children: React.ReactNode;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
export const ModalProvider: React.FC<Props> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [entityId, setEntityId] = useState("");
const [entityLabel, setEntityLabel] = useState("");
const [modalContentType, setModalContentType] = useState("");
const openModal = (
contentType: string,
entityId?: string,
entityLabel?: string,
) => {
setIsOpen(true);
setModalContentType(contentType);
if (entityId) setEntityId(entityId);
if (entityLabel) setEntityLabel(entityLabel);
};
const closeModal = () => {
setIsOpen(false);
};
return (
<ModalContext.Provider
value={{
isOpen,
openModal,
closeModal,
modalContentType,
entityId,
entityLabel,
}}
>
{children}
</ModalContext.Provider>
);
};
export const useModal = () => {
const context = useContext(ModalContext);
if (context === undefined) {
throw new Error("useModal must be used within a ModalProvider");
}
return context;
};

View File

@@ -0,0 +1,53 @@
import { createContext, useContext, useState } from "react";
type PopupContextType = {
isOpen: boolean;
showPopup: (params: { header: string; message: string }) => void;
hidePopup: () => void;
popupHeader: string;
popupMessage: string;
};
interface Props {
children: React.ReactNode;
}
const PopupContext = createContext<PopupContextType | undefined>(undefined);
export const PopupProvider: React.FC<Props> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [popupHeader, setPopupHeader] = useState("");
const [popupMessage, setPopupMessage] = useState("");
const showPopup = ({
header,
message,
}: {
header: string;
message: string;
}) => {
setIsOpen(true);
setPopupHeader(header);
setPopupMessage(message);
};
const hidePopup = () => {
setIsOpen(false);
};
return (
<PopupContext.Provider
value={{ isOpen, showPopup, hidePopup, popupHeader, popupMessage }}
>
{children}
</PopupContext.Provider>
);
};
export const usePopup = () => {
const context = useContext(PopupContext);
if (context === undefined) {
throw new Error("usePopup must be used within a PopupProvider");
}
return context;
};

View File

@@ -0,0 +1,69 @@
import React, {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
interface ThemeContextProps {
themePreference: "light" | "dark" | "system";
activeTheme: "light" | "dark";
switchTheme: (theme: "light" | "dark" | "system") => void;
}
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [themePreference, setThemePreference] = useState<
"light" | "dark" | "system"
>("system");
const [activeTheme, setActiveTheme] = useState<"light" | "dark">("light");
const switchTheme = (theme: "light" | "dark" | "system") => {
if (theme === "system") {
localStorage.removeItem("theme");
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setActiveTheme(isDark ? "dark" : "light");
document.documentElement.classList.toggle("dark", isDark);
} else {
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
localStorage.theme = theme;
setActiveTheme(isDark ? "dark" : "light");
}
setThemePreference(theme);
};
useEffect(() => {
if (!("theme" in localStorage)) {
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", isDark);
setActiveTheme(isDark ? "dark" : "light");
setThemePreference("system");
} else {
const isDark = localStorage.theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
setActiveTheme(isDark ? "dark" : "light");
setThemePreference(localStorage.theme as "light" | "dark");
}
}, []);
return (
<ThemeContext.Provider
value={{ switchTheme, themePreference, activeTheme }}
>
{themePreference.length ? children : null}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextProps => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};

View File

@@ -0,0 +1,114 @@
import React, {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
import { api } from "~/utils/api";
import { useRouter } from "next/navigation";
interface WorkspaceContextProps {
workspace: Workspace;
isLoading: boolean;
switchWorkspace: (_workspace: Workspace) => void;
availableWorkspaces: Workspace[];
}
interface Workspace {
name: string;
publicId: string;
}
const initialWorkspace: Workspace = {
name: "",
publicId: "",
};
const initialAvailableWorkspaces: Workspace[] = [];
const WorkspaceContext = createContext<WorkspaceContextProps | undefined>(
undefined,
);
export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const router = useRouter();
const [workspace, setWorkspace] = useState<Workspace>(initialWorkspace);
const [availableWorkspaces, setAvailableWorkspaces] = useState<Workspace[]>(
initialAvailableWorkspaces,
);
const { data, isLoading } = api.workspace.all.useQuery();
const switchWorkspace = (_workspace: Workspace) => {
localStorage.setItem("workspacePublicId", _workspace.publicId);
setWorkspace(_workspace);
router.push(`/boards`);
};
useEffect(() => {
if (!data) return;
const storedWorkspaceId: string | null =
localStorage.getItem("workspacePublicId");
if (data?.length) {
const workspaces = data
.map(({ workspace }) => {
if (!workspace) return;
return {
publicId: workspace.publicId,
name: workspace.name,
};
})
.filter((workspace) => workspace !== null) as Workspace[];
if (workspaces.length) setAvailableWorkspaces(workspaces);
}
if (storedWorkspaceId !== null) {
const newData = data;
const selectedWorkspace = newData?.find(
({ workspace }) => workspace?.publicId === storedWorkspaceId,
);
if (!selectedWorkspace?.workspace) return;
setWorkspace({
publicId: selectedWorkspace.workspace.publicId,
name: selectedWorkspace.workspace.name,
});
} else {
const primaryWorkspace = data?.[0]?.workspace;
if (!primaryWorkspace) return;
localStorage.setItem("workspacePublicId", primaryWorkspace?.publicId);
setWorkspace({
publicId: primaryWorkspace?.publicId,
name: primaryWorkspace?.name,
});
}
}, [data]);
return (
<WorkspaceContext.Provider
value={{ workspace, isLoading, availableWorkspaces, switchWorkspace }}
>
{children}
</WorkspaceContext.Provider>
);
};
export const useWorkspace = (): WorkspaceContextProps => {
const context = useContext(WorkspaceContext);
if (!context) {
throw new Error("useWorkspace must be used within a WorkspaceProvider");
}
return context;
};