feat: monorepo
This commit is contained in:
210
apps/web/src/providers/board.tsx
Normal file
210
apps/web/src/providers/board.tsx
Normal 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;
|
||||
};
|
||||
65
apps/web/src/providers/modal.tsx
Normal file
65
apps/web/src/providers/modal.tsx
Normal 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;
|
||||
};
|
||||
53
apps/web/src/providers/popup.tsx
Normal file
53
apps/web/src/providers/popup.tsx
Normal 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;
|
||||
};
|
||||
69
apps/web/src/providers/theme.tsx
Normal file
69
apps/web/src/providers/theme.tsx
Normal 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;
|
||||
};
|
||||
114
apps/web/src/providers/workspace.tsx
Normal file
114
apps/web/src/providers/workspace.tsx
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user