[MAJOR][FINAL] MVP
This commit is contained in:
126
src/components/AddTaskForm/AddTaskForm.component.scss
Executable file
126
src/components/AddTaskForm/AddTaskForm.component.scss
Executable file
@@ -0,0 +1,126 @@
|
||||
.c-AddTaskForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #444;
|
||||
background: #23232b;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__error {
|
||||
color: #ff4d4f;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__submit {
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(124, 58, 237, 0.08);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.c-AddTaskForm__submit:hover {
|
||||
background: #5b21b6;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__error {
|
||||
color: #ff4d4f;
|
||||
font-size: 0.95rem;
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__submit {
|
||||
flex: 1;
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(124, 58, 237, 0.08);
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
}
|
||||
.c-AddTaskForm__submit:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.c-AddTaskForm__submit:disabled {
|
||||
background: #5b21b6;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.c-AddTaskForm__cancel {
|
||||
flex: 1;
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: #23232b;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border 0.2s;
|
||||
}
|
||||
.c-AddTaskForm__cancel:hover {
|
||||
background: #2d2d3a;
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.c-AddTaskForm {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.c-AddTaskForm__actions {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.c-AddTaskForm__submit,
|
||||
.c-AddTaskForm__cancel {
|
||||
font-size: 0.95rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
}
|
||||
}
|
||||
8
src/components/AddTaskForm/AddTaskForm.test.ts
Executable file
8
src/components/AddTaskForm/AddTaskForm.test.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
import AddTaskForm from "./AddTaskForm"
|
||||
|
||||
describe("AddTaskForm", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
129
src/components/AddTaskForm/AddTaskForm.tsx
Executable file
129
src/components/AddTaskForm/AddTaskForm.tsx
Executable file
@@ -0,0 +1,129 @@
|
||||
import React, { useState, useRef, useEffect, useContext } from "react";
|
||||
import { API_ROUTES } from "@config/constants";
|
||||
import { Network } from "@utils";
|
||||
import { KaContext } from "src/contexts/KaContext";
|
||||
import "./AddTaskForm.component.scss"
|
||||
|
||||
interface AddTaskFormProps {
|
||||
statusOptions: StatusOption[];
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const AddTaskForm = (props: AddTaskFormProps): JSX.Element => {
|
||||
const { statusOptions, onCancel } = props;
|
||||
const { setModal, currentBoard, setCurrentBoard } = useContext(KaContext);
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [status, setStatus] = useState(statusOptions[0]?.value || "");
|
||||
const [error, setError] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const titleRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
titleRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// If statusOptions change, reset status to first
|
||||
setStatus(statusOptions[0]?.value || "");
|
||||
}, [statusOptions]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!title.trim()) {
|
||||
setError("Title is required.");
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const boardId = currentBoard?.id;
|
||||
if (!boardId) throw new Error("No board selected");
|
||||
const url = API_ROUTES.TASKS.replace(":boardId", String(boardId));
|
||||
const payload = {
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
status,
|
||||
};
|
||||
await Network.post(url, payload, {
|
||||
headers: { Authorization: "Bearer testtoken" },
|
||||
});
|
||||
setSubmitting(false);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setStatus(statusOptions[0]?.value || "");
|
||||
setModal({ open: false, title: "", body: null });
|
||||
setCurrentBoard({ ...currentBoard }); // Trigger refresh in parent
|
||||
} catch (err: any) {
|
||||
setSubmitting(false);
|
||||
setError(err.message || "Failed to add task");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setStatus(statusOptions[0]?.value || "");
|
||||
setError("");
|
||||
onCancel?.();
|
||||
setModal({ open: false, title: "", body: null });
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="c-AddTaskForm" onSubmit={handleSubmit} aria-label="Add New Task">
|
||||
<div className="c-AddTaskForm__field">
|
||||
<label htmlFor="task-title">Title<span style={{ color: 'red' }}>*</span></label>
|
||||
<input
|
||||
id="task-title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
required
|
||||
ref={titleRef}
|
||||
aria-describedby={error ? "task-title-error" : undefined}
|
||||
/>
|
||||
{error && <div id="task-title-error" className="c-AddTaskForm__error">{error}</div>}
|
||||
</div>
|
||||
<div className="c-AddTaskForm__field">
|
||||
<label htmlFor="task-desc">Description</label>
|
||||
<textarea
|
||||
id="task-desc"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="c-AddTaskForm__field">
|
||||
<label htmlFor="task-status">Status</label>
|
||||
<select
|
||||
id="task-status"
|
||||
value={status}
|
||||
onChange={e => setStatus(e.target.value)}
|
||||
>
|
||||
{statusOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="c-AddTaskForm__actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="c-AddTaskForm__submit"
|
||||
disabled={submitting}
|
||||
aria-busy={submitting}
|
||||
>
|
||||
{submitting ? "Adding..." : "Add Task"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="c-AddTaskForm__cancel"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTaskForm;
|
||||
3
src/components/AddTaskForm/index.ts
Executable file
3
src/components/AddTaskForm/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import AddTaskForm from "./AddTaskForm.jsx"
|
||||
|
||||
export default AddTaskForm
|
||||
@@ -1,3 +1,58 @@
|
||||
// Theme toggle switch styles
|
||||
.c-KaDrawer__sidebar-footer {
|
||||
background: var(--ka-bg-main);
|
||||
border-radius: 0.7rem;
|
||||
}
|
||||
.c-KaDrawer__theme-toggle-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1.2rem;
|
||||
gap: 1rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.c-KaDrawer__theme-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ka-color-font-disabled);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.c-KaDrawer__theme-label.active {
|
||||
color: var(--ka-color-primary);
|
||||
}
|
||||
.c-KaDrawer__theme-toggle-btn {
|
||||
background: var(--ka-color-primary-faded);
|
||||
border: none;
|
||||
border-radius: 1.2rem;
|
||||
width: 3.2rem;
|
||||
height: 1.7rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
.c-KaDrawer__theme-toggle-btn.light {
|
||||
background: var(--ka-color-primary-faded);
|
||||
}
|
||||
.c-KaDrawer__theme-toggle-btn.dark {
|
||||
background: var(--ka-color-primary);
|
||||
}
|
||||
.c-KaDrawer__theme-toggle-knob {
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
left: 0.15rem;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
transition: left 0.2s;
|
||||
}
|
||||
.c-KaDrawer__theme-toggle-btn.dark .c-KaDrawer__theme-toggle-knob {
|
||||
left: 1.65rem;
|
||||
}
|
||||
.c-KaDrawer {
|
||||
flex-basis: 20%;
|
||||
max-width: 35rem;
|
||||
|
||||
@@ -7,53 +7,34 @@ import "./KaDrawer.component.scss"
|
||||
|
||||
const USER_ID = 1
|
||||
|
||||
const KaDrawer = (): JSX.Element => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [boards, setBoards] = useState<any[]>([])
|
||||
const [newBoardTitle, setNewBoardTitle] = useState<string>("")
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initBoardAdd, setInitBoardAdd] = useState(false)
|
||||
const { theme, setTheme, currentBoard, setCurrentBoard } = useContext(KaContext)
|
||||
const createBoardTextRef = useRef<HTMLInputElement>(null)
|
||||
interface KaDrawerProps {
|
||||
boards: any[];
|
||||
currentBoard: any;
|
||||
setCurrentBoard: (board: any) => void;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
createBoard: (title: string) => void;
|
||||
}
|
||||
|
||||
const KaDrawer = ({ boards, loading, error, createBoard }: KaDrawerProps): JSX.Element => {
|
||||
const [newBoardTitle, setNewBoardTitle] = useState<string>("");
|
||||
const [initBoardAdd, setInitBoardAdd] = useState(false);
|
||||
const { theme, setTheme, currentBoard, setCurrentBoard } = useContext(KaContext);
|
||||
const createBoardTextRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Network.get(API_ROUTES.BOARDS.replace(":userId", String(USER_ID)), {
|
||||
headers: { Authorization: "Bearer testtoken" },
|
||||
})
|
||||
.then((data) => {
|
||||
setBoards(data.boards || [])
|
||||
setCurrentBoard(data.boards?.[0])
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
createBoardTextRef.current?.focus();
|
||||
}, [initBoardAdd]);
|
||||
|
||||
useEffect(() => {
|
||||
createBoardTextRef.current?.focus()
|
||||
}, [initBoardAdd])
|
||||
|
||||
const createBoard = () => {
|
||||
const handleCreateBoard = () => {
|
||||
if (!newBoardTitle.trim()) {
|
||||
alert("Board title cannot be empty");
|
||||
return;
|
||||
}
|
||||
Network.post(API_ROUTES.BOARDS.replace(":userId", String(USER_ID)), {
|
||||
name: newBoardTitle.trim(),
|
||||
}, {
|
||||
headers: { Authorization: "Bearer testtoken" },
|
||||
})
|
||||
.then((data) => {
|
||||
setBoards((prevBoards) => [...prevBoards, data.board]);
|
||||
setNewBoardTitle("");
|
||||
setInitBoardAdd(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
alert("Error creating board: " + err.message);
|
||||
});
|
||||
}
|
||||
createBoard(newBoardTitle);
|
||||
setNewBoardTitle("");
|
||||
setInitBoardAdd(false);
|
||||
};
|
||||
|
||||
return <aside className="c-KaDrawer d-flex flex-column">
|
||||
<div className="c-KaDrawer__logo d-flex align-items-center p-4">
|
||||
@@ -85,14 +66,37 @@ const KaDrawer = (): JSX.Element => {
|
||||
</>
|
||||
<div className="c-KaDrawer__create-board mt-3">
|
||||
{initBoardAdd ? <div className="add-board-container d-flex mx-3">
|
||||
<input ref={createBoardTextRef} onChange={e => setNewBoardTitle(e.target.value)} onBlur={() => setInitBoardAdd(false)} className="c-KaDrawer__add-board-text me-3" />
|
||||
<button className="c-KaDrawer__add-board-button" onMouseDown={createBoard}>Create</button>
|
||||
<input
|
||||
ref={createBoardTextRef}
|
||||
onChange={e => setNewBoardTitle(e.target.value)}
|
||||
onBlur={() => setInitBoardAdd(false)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleCreateBoard();
|
||||
}
|
||||
}}
|
||||
className="c-KaDrawer__add-board-text me-3"
|
||||
/>
|
||||
<button className="c-KaDrawer__add-board-button" onMouseDown={handleCreateBoard}>Create</button>
|
||||
</div> : <li className="c-KaDrawer__board cursor-pointer create ps-5 py-2" onClick={() => setInitBoardAdd(true)}>+ Create New Board</li>}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="c-KaDrawer__sidebar-footer mt-auto">
|
||||
<button className="c-KaDrawer__theme-toggle btn btn-outline-secondary" aria-label="Toggle theme" onClick={() => setTheme(theme === KaTheme.DARK ? KaTheme.LIGHT : KaTheme.DARK)} />
|
||||
<div className="d-flex w-100 px-5">
|
||||
<div className="c-KaDrawer__sidebar-footer mt-auto mb-4">
|
||||
<div className="c-KaDrawer__theme-toggle-switch">
|
||||
<span className={`c-KaDrawer__theme-label${theme === KaTheme.LIGHT ? ' active' : ''}`}>LIGHT</span>
|
||||
<button
|
||||
className={`c-KaDrawer__theme-toggle-btn${theme === KaTheme.DARK ? ' dark' : ' light'}`}
|
||||
aria-label="Toggle theme"
|
||||
onClick={() => setTheme(theme === KaTheme.DARK ? KaTheme.LIGHT : KaTheme.DARK)}
|
||||
>
|
||||
<span className="c-KaDrawer__theme-toggle-knob" />
|
||||
</button>
|
||||
<span className={`c-KaDrawer__theme-label${theme === KaTheme.DARK ? ' active' : ''}`}>DARK</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
.c-KaHeader {
|
||||
.menuBtn {
|
||||
background: var(--ka-color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
font-size: 1.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
.menuBtn:hover,
|
||||
.menuBtn:focus {
|
||||
background: var(--ka-color-primary-faded);
|
||||
box-shadow: 0 2px 8px 0 rgba(99, 95, 199, 0.12);
|
||||
}
|
||||
background: var(--ka-bg-sidebar);
|
||||
color: var(--ka-color-font);
|
||||
display: flex;
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
import React from "react"
|
||||
import React, { useContext } from "react"
|
||||
import { KaContext } from "src/contexts/KaContext";
|
||||
import AddTaskForm from "@components/AddTaskForm";
|
||||
import "./KaHeader.component.scss"
|
||||
|
||||
interface KaHeaderProps { }
|
||||
|
||||
interface KaHeaderProps {
|
||||
onMenuClick?: () => void;
|
||||
menuOpen?: boolean;
|
||||
statusOptions: StatusOption[];
|
||||
}
|
||||
|
||||
const KaHeader = (props: KaHeaderProps): JSX.Element => {
|
||||
const { currentBoard, setModal } = useContext(KaContext);
|
||||
const { onMenuClick, menuOpen, statusOptions } = props;
|
||||
const [isMobile, setIsMobile] = React.useState(window.innerWidth < 768);
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
return (
|
||||
<header className="c-KaHeader d-flex justify-content-between align-items-center px-3 py-4">
|
||||
<span className="boardTitle">Platform Launch</span>
|
||||
<button className="addTaskBtn py-2 px-3 cursor-pointer">+ Add New Task</button>
|
||||
<header className="c-KaHeader d-flex justify-content-between align-items-center px-3 py-4 mw-100">
|
||||
{onMenuClick && (
|
||||
<button className="menuBtn me-3 p-2" onClick={onMenuClick} aria-label={menuOpen ? 'Close menu' : 'Open menu'}>
|
||||
{menuOpen ? <span>✕</span> : <span>☰</span>}
|
||||
</button>
|
||||
)}
|
||||
<span className="boardTitle">{currentBoard?.name || "Select a board"}</span>
|
||||
<button className="addTaskBtn py-2 px-3 cursor-pointer" onClick={() => setModal({
|
||||
open: true,
|
||||
title: "Add New Task",
|
||||
body: <AddTaskForm statusOptions={statusOptions} />,
|
||||
})}>+{isMobile ? "" : " Add New Task"}</button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
.c-KaMain {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const KaMain = (): JSX.Element => {
|
||||
}
|
||||
}, [currentBoard]);
|
||||
|
||||
return <main className="c-KaMain d-flex p-3 flex-grow-1">
|
||||
return <main className="c-KaMain d-flex p-3 flex-grow-1 mw-10 overflow-auto">
|
||||
{taskGroups?.map(taskGroup => <TaskList taskGroup={taskGroup} />)}
|
||||
{/* Add a task list to add new list */}
|
||||
<TaskList type="add-new" taskGroup={{ groupId: "Add New List", tasks: [] }} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.c-TaskList {
|
||||
width: 20rem;
|
||||
flex: 0 0 20rem;
|
||||
.list-color {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
@@ -11,6 +12,7 @@
|
||||
background-color: var(--ka-bg-sidebar-darker);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
margin-top: 1.75rem;
|
||||
&:hover {
|
||||
.add-new-list-btn {
|
||||
background-color: var(--ka-bg-hover);
|
||||
|
||||
@@ -10,8 +10,8 @@ const TaskList = (props: TaskListProps): JSX.Element => {
|
||||
const { taskGroup, type } = props;
|
||||
// Generate a random pastel color for the list-color indicator
|
||||
const randomColor = `hsl(${Math.floor(Math.random() * 360)}, 70%, 70%)`;
|
||||
return <div className="c-TaskList h-100 mx-2">
|
||||
{type === "add-new" ? <div className="c-TaskList__new flex-center h-100">
|
||||
return <div className="c-TaskList h-100 mx-2 d-flex flex-column">
|
||||
{type === "add-new" ? <div className="c-TaskList__new flex-center h-100 flex-grow-1">
|
||||
<span className="add-new-list-btn py-2 px-3 cursor-pointer">+ Add New List</span>
|
||||
</div>
|
||||
:
|
||||
|
||||
@@ -5,6 +5,7 @@ import KaDrawer from "./KaDrawer"
|
||||
import TaskList from "./TaskList"
|
||||
import TaskItem from "./TaskItem"
|
||||
import KaModal from "./KaModal"
|
||||
import AddTaskForm from "./AddTaskForm"
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
@@ -14,4 +15,5 @@ export {
|
||||
TaskList,
|
||||
TaskItem,
|
||||
KaModal,
|
||||
AddTaskForm,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export const API_ROUTES = {
|
||||
BOARD: `${API_BASE_URL}/boards/:userId/:boardId`,
|
||||
TASKS: `${API_BASE_URL}/tasks/:boardId`,
|
||||
TASK: `${API_BASE_URL}/tasks/:boardId/:taskId`,
|
||||
TASKLISTS: `${API_BASE_URL}/task-lists`,
|
||||
TASKLIST: `${API_BASE_URL}/task-lists/:taskId`,
|
||||
USERS: `${API_BASE_URL}/users`,
|
||||
USER: `${API_BASE_URL}/users/:id`,
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@ import React from "react"
|
||||
|
||||
export type KaTheme = "ka-dark" | "ka-light"
|
||||
export interface KaContextType {
|
||||
theme: KaTheme
|
||||
setTheme: (theme: KaTheme) => void
|
||||
currentBoard?: Board
|
||||
setCurrentBoard: (board: Board) => void
|
||||
modal: {
|
||||
open: boolean
|
||||
body?: React.ReactNode
|
||||
title?: string
|
||||
[key: string]: any
|
||||
}
|
||||
setModal: (modal: Partial<KaContextType['modal']>) => void
|
||||
theme: KaTheme;
|
||||
setTheme: (theme: KaTheme) => void;
|
||||
currentBoard?: Board;
|
||||
setCurrentBoard: (board: Board) => void;
|
||||
modal: KaModalState;
|
||||
setModal: (modal: Partial<KaModalState>) => void;
|
||||
mobileDrawerOpen: boolean;
|
||||
setMobileDrawerOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const KaContext = React.createContext<KaContextType>({
|
||||
@@ -20,5 +17,7 @@ export const KaContext = React.createContext<KaContextType>({
|
||||
setTheme: () => { },
|
||||
setCurrentBoard: (board: Board) => { },
|
||||
modal: { open: false },
|
||||
setModal: () => { }
|
||||
setModal: () => { },
|
||||
mobileDrawerOpen: false,
|
||||
setMobileDrawerOpen: () => { }
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ export const KaProvider: React.FC<KaProviderProps> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<KaTheme>("ka-dark")
|
||||
const [currentBoard, setCurrentBoard] = useState<Board>()
|
||||
const [modal, setModalState] = useState<{ open: boolean; body?: React.ReactNode; title?: string;[key: string]: any }>({ open: false })
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("ar-theme", theme)
|
||||
@@ -20,7 +21,7 @@ export const KaProvider: React.FC<KaProviderProps> = ({ children }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<KaContext.Provider value={{ theme, setTheme, currentBoard, setCurrentBoard, modal, setModal }}>
|
||||
<KaContext.Provider value={{ theme, setTheme, currentBoard, setCurrentBoard, modal, setModal, mobileDrawerOpen, setMobileDrawerOpen }}>
|
||||
{children}
|
||||
</KaContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,19 +1,86 @@
|
||||
|
||||
|
||||
import React from "react"
|
||||
import { KaDrawer, KaHeader, KaMain, KaModal } from "@components"
|
||||
import React from "react";
|
||||
import { API_ROUTES } from "@config/constants";
|
||||
import { Network } from "@utils";
|
||||
import { KaContext } from "src/contexts/KaContext";
|
||||
import { KaDrawer, KaHeader, KaMain, KaModal } from "@components";
|
||||
|
||||
|
||||
const USER_ID = 1;
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { mobileDrawerOpen, setMobileDrawerOpen, setCurrentBoard } = React.useContext(KaContext);
|
||||
const [isMobile, setIsMobile] = React.useState(window.innerWidth < 768);
|
||||
const [boards, setBoards] = React.useState<any[]>([]);
|
||||
const [currentBoard, setCurrentBoardLocal] = React.useState<any>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
Network.get(API_ROUTES.BOARDS.replace(":userId", String(USER_ID)), {
|
||||
headers: { Authorization: "Bearer testtoken" },
|
||||
})
|
||||
.then((data) => {
|
||||
setBoards(data.boards || []);
|
||||
setCurrentBoardLocal(data.boards?.[0] || null);
|
||||
setCurrentBoard(data.boards?.[0] || null);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [setCurrentBoard]);
|
||||
|
||||
// Example: statusOptions from currentBoard
|
||||
const statusOptions: StatusOption[] = currentBoard?.statuses?.map((s: any) => ({ value: s.value, label: s.label })) || [
|
||||
{ value: "todo", label: "To Do" },
|
||||
{ value: "in_progress", label: "In Progress" },
|
||||
{ value: "done", label: "Done" },
|
||||
];
|
||||
|
||||
const handleCreateBoard = async (title: string) => {
|
||||
if (!title.trim()) return;
|
||||
try {
|
||||
const res = await Network.post(API_ROUTES.BOARDS.replace(":userId", String(USER_ID)), { name: title.trim() }, {
|
||||
headers: { Authorization: "Bearer testtoken" },
|
||||
});
|
||||
setBoards(prev => [...prev, res.board]);
|
||||
setCurrentBoardLocal(res.board);
|
||||
setCurrentBoard(res.board);
|
||||
} catch (err) {
|
||||
alert("Error creating board: " + ((err as any).message || err));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="c-Home d-flex" style={{ height: '100vh' }}>
|
||||
<KaDrawer />
|
||||
<div className="c-Home__main d-flex flex-column flex-grow-1">
|
||||
<KaHeader />
|
||||
<div className="c-Home d-flex mw-100" style={{ height: '100vh', overflowY: 'hidden' }}>
|
||||
{(!isMobile || mobileDrawerOpen) && <KaDrawer
|
||||
boards={boards}
|
||||
currentBoard={currentBoard}
|
||||
setCurrentBoard={setCurrentBoardLocal}
|
||||
loading={loading}
|
||||
error={error}
|
||||
createBoard={handleCreateBoard}
|
||||
/>}
|
||||
<div className="c-Home__main d-flex flex-column flex-grow-1 mw-100">
|
||||
<KaHeader
|
||||
onMenuClick={isMobile ? () => setMobileDrawerOpen(!mobileDrawerOpen) : undefined}
|
||||
menuOpen={mobileDrawerOpen}
|
||||
statusOptions={statusOptions}
|
||||
/>
|
||||
<KaMain />
|
||||
</div>
|
||||
<KaModal />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Home
|
||||
export default Home;
|
||||
12
src/types/entity.d.ts
vendored
12
src/types/entity.d.ts
vendored
@@ -1,3 +1,10 @@
|
||||
interface KaModalState {
|
||||
open: boolean
|
||||
body?: React.ReactNode
|
||||
title?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Board {
|
||||
id: number
|
||||
userId: number
|
||||
@@ -21,3 +28,8 @@ interface TaskGroup {
|
||||
groupId: string
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
type StatusOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user