Skip to content

Commit 86329a5

Browse files
committed
feat: frontend 1.0
1 parent 66c0d26 commit 86329a5

40 files changed

Lines changed: 1347 additions & 20 deletions

frontend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@fortawesome/fontawesome-svg-core": "^7.0.0",
14+
"@fortawesome/free-solid-svg-icons": "^7.0.0",
15+
"@fortawesome/react-fontawesome": "^3.0.1",
1316
"@helia/http": "^2.2.1",
1417
"@libp2p/interfaces": "^3.3.2",
18+
"@tanstack/react-query": "^5.85.5",
1519
"kubo-rpc-client": "^5.2.0",
1620
"react": "^19.1.0",
1721
"react-dom": "^19.1.0"

frontend/src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import ThemeProvider from './context/ThemeContext';
2+
import LoadingScreenProvider from './context/LoadingScreenContext';
23
import MainPage from './pages/MainPage';
4+
import AlertProvider from './context/AlertContext';
35

46
function App() {
57

68
return (
79
<ThemeProvider>
8-
<MainPage/>
10+
<LoadingScreenProvider>
11+
<AlertProvider>
12+
<MainPage/>
13+
</AlertProvider>
14+
</LoadingScreenProvider>
915
</ThemeProvider>
1016
)
1117
}

frontend/src/components/Alert.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useEffect } from "react";
2+
import '../styles/Alert.css'
3+
4+
export type AlertType = 'success' | 'info' | 'warning' | 'error';
5+
6+
type AlertProps = {
7+
type: AlertType,
8+
message: String,
9+
duration?: number,
10+
onClose: () => void
11+
}
12+
13+
function Alert({ type = 'info', message, duration = 3000, onClose }: AlertProps) {
14+
useEffect(() => {
15+
if (duration > 0) {
16+
const timer = setTimeout(() => {
17+
onClose?.();
18+
}, duration);
19+
return () => clearTimeout(timer);
20+
}
21+
}, [duration, onClose]);
22+
23+
return (
24+
<div className={`alert alert-${type}`}>
25+
{type + ": " + message}
26+
</div>
27+
);
28+
}
29+
30+
export default Alert;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import '../styles/Buttons.css'
2+
3+
type ButtonProps = {
4+
children: React.ReactNode;
5+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
6+
disabled?: boolean;
7+
};
8+
9+
export function FilledButton({ children, onClick, disabled }: ButtonProps) {
10+
return (
11+
<button
12+
className="button-filled"
13+
onClick={onClick}
14+
disabled={disabled}
15+
>
16+
{children}
17+
</button>
18+
);
19+
}
20+
21+
export function EmptyButton({ children, onClick, disabled }: ButtonProps) {
22+
return (
23+
<button
24+
className="button-empty"
25+
onClick={onClick}
26+
disabled={disabled}
27+
>
28+
{children}
29+
</button>
30+
);
31+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { useState } from "react";
2+
import Popup from "./PopUp";
3+
import { TaskDifficulty } from "../../../objects/BoardTask";
4+
import "../styles/CreateTaskPopUp.css"
5+
6+
type TaskFormData = {
7+
title: string;
8+
description: string;
9+
difficulty: string;
10+
tags: string[];
11+
requirements: string[];
12+
resources: File[]; // files for upload
13+
token: {
14+
name: string;
15+
description: string;
16+
image: File | null; // image upload
17+
};
18+
};
19+
20+
type CreateTaskPopUpProps = {
21+
onSubmit: (data: TaskFormData) => void;
22+
isOpen: boolean;
23+
onClose: () => void;
24+
}
25+
26+
function CreateTaskPopUp({ onSubmit, isOpen, onClose }: CreateTaskPopUpProps) {
27+
const [form, setForm] = useState<TaskFormData>({
28+
title: "",
29+
description: "",
30+
difficulty: "Beginner",
31+
tags: [],
32+
requirements: [],
33+
resources: [],
34+
token: { name: "", description: "", image: null },
35+
});
36+
37+
const handleChange = (field: keyof TaskFormData, value: any) => {
38+
setForm((prev) => ({ ...prev, [field]: value }));
39+
};
40+
41+
const handleTokenChange = (field: keyof TaskFormData["token"], value: any) => {
42+
setForm((prev) => ({
43+
...prev,
44+
token: { ...prev.token, [field]: value },
45+
}));
46+
};
47+
48+
const handleSubmit = (e: React.FormEvent) => {
49+
e.preventDefault();
50+
onSubmit(form);
51+
};
52+
53+
return (
54+
<Popup isOpen={isOpen} onClose={onClose} title="Create a New Task">
55+
<form className="task-form" onSubmit={handleSubmit}>
56+
<label>
57+
Title
58+
<input type="text" value={form.title} onChange={(e) => handleChange("title", e.target.value)} required />
59+
</label>
60+
61+
<label>
62+
Description
63+
<textarea value={form.description} onChange={(e) => handleChange("description", e.target.value)} />
64+
</label>
65+
66+
<label>
67+
Difficulty
68+
<select value={form.difficulty} onChange={(e) => handleChange("difficulty", e.target.value)}>
69+
{Object.values(TaskDifficulty).map((diff) => (
70+
<option key={diff} value={diff}>
71+
{diff}
72+
</option>
73+
))}
74+
</select>
75+
</label>
76+
77+
<label>
78+
Tags (comma separated)
79+
<input type="text" value={form.tags} onChange={(e) => handleChange("tags", e.target.value.split(",").map((tag) => tag.trim()))} />
80+
</label>
81+
82+
<label>
83+
Requirements (comma separated)
84+
<input
85+
type="text"
86+
value={form.requirements.join(", ")}
87+
onChange={(e) =>
88+
handleChange(
89+
"requirements",
90+
e.target.value.split(",").map((req) => req.trim())
91+
)
92+
}
93+
/>
94+
</label>
95+
96+
<label>
97+
Resources (one per line, e.g. IPFS links)
98+
<input
99+
type="file"
100+
multiple
101+
onChange={(e) =>
102+
handleChange("resources", e.target.files ? Array.from(e.target.files) : [])
103+
}
104+
/>
105+
</label>
106+
107+
<h3>Token Metadata</h3>
108+
109+
<label>
110+
Token Name:
111+
<input
112+
type="text"
113+
value={form.token.name}
114+
onChange={(e) => handleTokenChange("name", e.target.value)}
115+
required
116+
/>
117+
</label>
118+
119+
<label>
120+
Token Description:
121+
<textarea
122+
value={form.token.description}
123+
onChange={(e) => handleTokenChange("description", e.target.value)}
124+
/>
125+
</label>
126+
127+
<label>
128+
Token Image:
129+
<input
130+
type="file"
131+
accept="image/*"
132+
onChange={(e) =>
133+
handleTokenChange("image", e.target.files ? e.target.files[0] : null)
134+
}
135+
required
136+
/>
137+
</label>
138+
139+
<button type="submit">
140+
Create Task
141+
</button>
142+
</form>
143+
</Popup>
144+
);
145+
}
146+
147+
export default CreateTaskPopUp;

frontend/src/components/Footer.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import '../styles/Footer.css'
2+
3+
function Footer() {
4+
return (
5+
<div className="footer-container">
6+
<p>TaskBoard</p>
7+
<p><span>Author: </span>Miški</p>
8+
</div>
9+
);
10+
}
11+
12+
export default Footer;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2+
import { faPlus } from '@fortawesome/free-solid-svg-icons';
3+
import '../styles/LoadingScreen.css'
4+
5+
6+
function LoadingScreen({ message }: {message?: string}) {
7+
return (
8+
<div className="loading-overlay">
9+
<div className="loading-content">
10+
<div className="loading-spinner">
11+
<FontAwesomeIcon icon={faPlus} />
12+
</div>
13+
<div className="loading-message">
14+
{message ?? "Loading ..."}
15+
</div>
16+
</div>
17+
</div>
18+
);
19+
}
20+
21+
export default LoadingScreen;

frontend/src/components/PopUp.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from "react";
2+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3+
import { faXmark } from "@fortawesome/free-solid-svg-icons";
4+
import "../styles/PopUp.css";
5+
6+
type PopupProps = {
7+
isOpen: boolean;
8+
onClose: () => void;
9+
title?: string;
10+
children: React.ReactNode;
11+
};
12+
13+
function Popup({ isOpen, onClose, title, children }: PopupProps) {
14+
if (!isOpen) return null;
15+
16+
return (
17+
<div className="popup-overlay" onClick={onClose}>
18+
<div
19+
className="popup-content"
20+
onClick={(e) => e.stopPropagation()} // prevent closing when clicking inside
21+
>
22+
{title && <h2 className="popup-title">{title}</h2>}
23+
<div className="popup-body">{children}</div>
24+
<button className="popup-close-btn" onClick={onClose}>
25+
<FontAwesomeIcon icon={faXmark} />
26+
</button>
27+
</div>
28+
</div>
29+
);
30+
};
31+
32+
export default Popup;

frontend/src/components/SearchBar.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2+
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
3+
import { TaskDifficulty } from "../../../objects/BoardTask";
4+
import '../styles/SearchBar.css'
15

6+
type SearchBarProps = {
7+
value: string,
8+
onValueChange: (newValue: string) => void,
9+
difficulty: string,
10+
onDifficultyChange: (newDifficulty: string) => void,
11+
onSubmit: () => void
12+
}
213

3-
function SearchBar() {
4-
14+
function SearchBar({ value, onValueChange, difficulty, onDifficultyChange, onSubmit }: SearchBarProps) {
515

616
return (
717
<div className="search-bar">
8-
18+
<input type="text" placeholder={"Search task ..."} value={value} onChange={(e) => onValueChange(e.target.value)}/>
19+
<select value={difficulty} onChange={(e) => onDifficultyChange(e.target.value)}>
20+
<option value="">All</option>
21+
{Object.values(TaskDifficulty).map((diff) => (
22+
<option key={diff} value={diff}>
23+
{diff}
24+
</option>
25+
))}
26+
</select>
27+
<button onClick={onSubmit}><FontAwesomeIcon icon={faMagnifyingGlass} /></button>
928
</div>
1029
);
1130
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useState } from "react";
2+
import Popup from "./PopUp";
3+
import "../styles/SolutionPopUp.css"
4+
5+
type SolutionPopUpProps = {
6+
onSubmit: (data: string) => void;
7+
isOpen: boolean;
8+
onClose: () => void;
9+
}
10+
11+
function SolutionPopUp({ onSubmit, isOpen, onClose }: SolutionPopUpProps) {
12+
const [solution, setSolution] = useState<string>("")
13+
14+
const handleSubmit = (e: React.FormEvent) => {
15+
e.preventDefault();
16+
onSubmit(solution);
17+
};
18+
19+
return (
20+
<Popup isOpen={isOpen} onClose={onClose} title="Provide Soluton">
21+
<form className="solution-form" onSubmit={handleSubmit}>
22+
<label>
23+
Secreat
24+
<input type="text" value={solution} onChange={(e) => setSolution(e.target.value)} required />
25+
</label>
26+
<button type="submit">
27+
Submit solution
28+
</button>
29+
</form>
30+
</Popup>
31+
);
32+
}
33+
34+
export default SolutionPopUp;

0 commit comments

Comments
 (0)