[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 {
|
.c-KaDrawer {
|
||||||
flex-basis: 20%;
|
flex-basis: 20%;
|
||||||
max-width: 35rem;
|
max-width: 35rem;
|
||||||
|
|||||||
@@ -7,53 +7,34 @@ import "./KaDrawer.component.scss"
|
|||||||
|
|
||||||
const USER_ID = 1
|
const USER_ID = 1
|
||||||
|
|
||||||
const KaDrawer = (): JSX.Element => {
|
interface KaDrawerProps {
|
||||||
const [loading, setLoading] = useState(true)
|
boards: any[];
|
||||||
const [boards, setBoards] = useState<any[]>([])
|
currentBoard: any;
|
||||||
const [newBoardTitle, setNewBoardTitle] = useState<string>("")
|
setCurrentBoard: (board: any) => void;
|
||||||
const [error, setError] = useState<string | null>(null)
|
loading: boolean;
|
||||||
const [initBoardAdd, setInitBoardAdd] = useState(false)
|
error: string | null;
|
||||||
const { theme, setTheme, currentBoard, setCurrentBoard } = useContext(KaContext)
|
createBoard: (title: string) => void;
|
||||||
const createBoardTextRef = useRef<HTMLInputElement>(null)
|
}
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
Network.get(API_ROUTES.BOARDS.replace(":userId", String(USER_ID)), {
|
createBoardTextRef.current?.focus();
|
||||||
headers: { Authorization: "Bearer testtoken" },
|
}, [initBoardAdd]);
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
setBoards(data.boards || [])
|
|
||||||
setCurrentBoard(data.boards?.[0])
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setError(err.message)
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const handleCreateBoard = () => {
|
||||||
createBoardTextRef.current?.focus()
|
|
||||||
}, [initBoardAdd])
|
|
||||||
|
|
||||||
const createBoard = () => {
|
|
||||||
if (!newBoardTitle.trim()) {
|
if (!newBoardTitle.trim()) {
|
||||||
alert("Board title cannot be empty");
|
alert("Board title cannot be empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Network.post(API_ROUTES.BOARDS.replace(":userId", String(USER_ID)), {
|
createBoard(newBoardTitle);
|
||||||
name: newBoardTitle.trim(),
|
setNewBoardTitle("");
|
||||||
}, {
|
setInitBoardAdd(false);
|
||||||
headers: { Authorization: "Bearer testtoken" },
|
};
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
setBoards((prevBoards) => [...prevBoards, data.board]);
|
|
||||||
setNewBoardTitle("");
|
|
||||||
setInitBoardAdd(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
alert("Error creating board: " + err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return <aside className="c-KaDrawer d-flex flex-column">
|
return <aside className="c-KaDrawer d-flex flex-column">
|
||||||
<div className="c-KaDrawer__logo d-flex align-items-center p-4">
|
<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">
|
<div className="c-KaDrawer__create-board mt-3">
|
||||||
{initBoardAdd ? <div className="add-board-container d-flex mx-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" />
|
<input
|
||||||
<button className="c-KaDrawer__add-board-button" onMouseDown={createBoard}>Create</button>
|
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> : <li className="c-KaDrawer__board cursor-pointer create ps-5 py-2" onClick={() => setInitBoardAdd(true)}>+ Create New Board</li>}
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="c-KaDrawer__sidebar-footer mt-auto">
|
<div className="d-flex w-100 px-5">
|
||||||
<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="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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,24 @@
|
|||||||
.c-KaHeader {
|
.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);
|
background: var(--ka-bg-sidebar);
|
||||||
color: var(--ka-color-font);
|
color: var(--ka-color-font);
|
||||||
display: flex;
|
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"
|
import "./KaHeader.component.scss"
|
||||||
|
|
||||||
interface KaHeaderProps { }
|
|
||||||
|
interface KaHeaderProps {
|
||||||
|
onMenuClick?: () => void;
|
||||||
|
menuOpen?: boolean;
|
||||||
|
statusOptions: StatusOption[];
|
||||||
|
}
|
||||||
|
|
||||||
const KaHeader = (props: KaHeaderProps): JSX.Element => {
|
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 (
|
return (
|
||||||
<header className="c-KaHeader d-flex justify-content-between align-items-center px-3 py-4">
|
<header className="c-KaHeader d-flex justify-content-between align-items-center px-3 py-4 mw-100">
|
||||||
<span className="boardTitle">Platform Launch</span>
|
{onMenuClick && (
|
||||||
<button className="addTaskBtn py-2 px-3 cursor-pointer">+ Add New Task</button>
|
<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>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
.c-KaMain {
|
.c-KaMain {
|
||||||
|
overflow-y: hidden !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const KaMain = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}, [currentBoard]);
|
}, [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} />)}
|
{taskGroups?.map(taskGroup => <TaskList taskGroup={taskGroup} />)}
|
||||||
{/* Add a task list to add new list */}
|
{/* Add a task list to add new list */}
|
||||||
<TaskList type="add-new" taskGroup={{ groupId: "Add New List", tasks: [] }} />
|
<TaskList type="add-new" taskGroup={{ groupId: "Add New List", tasks: [] }} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.c-TaskList {
|
.c-TaskList {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
|
flex: 0 0 20rem;
|
||||||
.list-color {
|
.list-color {
|
||||||
width: 0.75rem;
|
width: 0.75rem;
|
||||||
height: 0.75rem;
|
height: 0.75rem;
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
background-color: var(--ka-bg-sidebar-darker);
|
background-color: var(--ka-bg-sidebar-darker);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-top: 1.75rem;
|
||||||
&:hover {
|
&:hover {
|
||||||
.add-new-list-btn {
|
.add-new-list-btn {
|
||||||
background-color: var(--ka-bg-hover);
|
background-color: var(--ka-bg-hover);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const TaskList = (props: TaskListProps): JSX.Element => {
|
|||||||
const { taskGroup, type } = props;
|
const { taskGroup, type } = props;
|
||||||
// Generate a random pastel color for the list-color indicator
|
// Generate a random pastel color for the list-color indicator
|
||||||
const randomColor = `hsl(${Math.floor(Math.random() * 360)}, 70%, 70%)`;
|
const randomColor = `hsl(${Math.floor(Math.random() * 360)}, 70%, 70%)`;
|
||||||
return <div className="c-TaskList h-100 mx-2">
|
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">
|
{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>
|
<span className="add-new-list-btn py-2 px-3 cursor-pointer">+ Add New List</span>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import KaDrawer from "./KaDrawer"
|
|||||||
import TaskList from "./TaskList"
|
import TaskList from "./TaskList"
|
||||||
import TaskItem from "./TaskItem"
|
import TaskItem from "./TaskItem"
|
||||||
import KaModal from "./KaModal"
|
import KaModal from "./KaModal"
|
||||||
|
import AddTaskForm from "./AddTaskForm"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
/* PLOP_INJECT_EXPORT */
|
/* PLOP_INJECT_EXPORT */
|
||||||
@@ -14,4 +15,5 @@ export {
|
|||||||
TaskList,
|
TaskList,
|
||||||
TaskItem,
|
TaskItem,
|
||||||
KaModal,
|
KaModal,
|
||||||
|
AddTaskForm,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export const API_ROUTES = {
|
|||||||
BOARD: `${API_BASE_URL}/boards/:userId/:boardId`,
|
BOARD: `${API_BASE_URL}/boards/:userId/:boardId`,
|
||||||
TASKS: `${API_BASE_URL}/tasks/:boardId`,
|
TASKS: `${API_BASE_URL}/tasks/:boardId`,
|
||||||
TASK: `${API_BASE_URL}/tasks/:boardId/:taskId`,
|
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`,
|
USERS: `${API_BASE_URL}/users`,
|
||||||
USER: `${API_BASE_URL}/users/:id`,
|
USER: `${API_BASE_URL}/users/:id`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ import React from "react"
|
|||||||
|
|
||||||
export type KaTheme = "ka-dark" | "ka-light"
|
export type KaTheme = "ka-dark" | "ka-light"
|
||||||
export interface KaContextType {
|
export interface KaContextType {
|
||||||
theme: KaTheme
|
theme: KaTheme;
|
||||||
setTheme: (theme: KaTheme) => void
|
setTheme: (theme: KaTheme) => void;
|
||||||
currentBoard?: Board
|
currentBoard?: Board;
|
||||||
setCurrentBoard: (board: Board) => void
|
setCurrentBoard: (board: Board) => void;
|
||||||
modal: {
|
modal: KaModalState;
|
||||||
open: boolean
|
setModal: (modal: Partial<KaModalState>) => void;
|
||||||
body?: React.ReactNode
|
mobileDrawerOpen: boolean;
|
||||||
title?: string
|
setMobileDrawerOpen: (open: boolean) => void;
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
setModal: (modal: Partial<KaContextType['modal']>) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KaContext = React.createContext<KaContextType>({
|
export const KaContext = React.createContext<KaContextType>({
|
||||||
@@ -20,5 +17,7 @@ export const KaContext = React.createContext<KaContextType>({
|
|||||||
setTheme: () => { },
|
setTheme: () => { },
|
||||||
setCurrentBoard: (board: Board) => { },
|
setCurrentBoard: (board: Board) => { },
|
||||||
modal: { open: false },
|
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 [theme, setTheme] = useState<KaTheme>("ka-dark")
|
||||||
const [currentBoard, setCurrentBoard] = useState<Board>()
|
const [currentBoard, setCurrentBoard] = useState<Board>()
|
||||||
const [modal, setModalState] = useState<{ open: boolean; body?: React.ReactNode; title?: string;[key: string]: any }>({ open: false })
|
const [modal, setModalState] = useState<{ open: boolean; body?: React.ReactNode; title?: string;[key: string]: any }>({ open: false })
|
||||||
|
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute("ar-theme", theme)
|
document.documentElement.setAttribute("ar-theme", theme)
|
||||||
@@ -20,7 +21,7 @@ export const KaProvider: React.FC<KaProviderProps> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KaContext.Provider value={{ theme, setTheme, currentBoard, setCurrentBoard, modal, setModal }}>
|
<KaContext.Provider value={{ theme, setTheme, currentBoard, setCurrentBoard, modal, setModal, mobileDrawerOpen, setMobileDrawerOpen }}>
|
||||||
{children}
|
{children}
|
||||||
</KaContext.Provider>
|
</KaContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,86 @@
|
|||||||
|
|
||||||
|
|
||||||
import React from "react"
|
import React from "react";
|
||||||
import { KaDrawer, KaHeader, KaMain, KaModal } from "@components"
|
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 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 (
|
return (
|
||||||
<div className="c-Home d-flex" style={{ height: '100vh' }}>
|
<div className="c-Home d-flex mw-100" style={{ height: '100vh', overflowY: 'hidden' }}>
|
||||||
<KaDrawer />
|
{(!isMobile || mobileDrawerOpen) && <KaDrawer
|
||||||
<div className="c-Home__main d-flex flex-column flex-grow-1">
|
boards={boards}
|
||||||
<KaHeader />
|
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 />
|
<KaMain />
|
||||||
</div>
|
</div>
|
||||||
<KaModal />
|
<KaModal />
|
||||||
</div>
|
</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 {
|
interface Board {
|
||||||
id: number
|
id: number
|
||||||
userId: number
|
userId: number
|
||||||
@@ -21,3 +28,8 @@ interface TaskGroup {
|
|||||||
groupId: string
|
groupId: string
|
||||||
tasks: Task[]
|
tasks: Task[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatusOption = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user