Updating to support categories
This commit is contained in:
@@ -5,15 +5,17 @@ import ProtectedRoute from './components/ProtectedRoute';
|
||||
import RandomSignUpload from './components/RandomVideoUpload';
|
||||
import SignDetailpage from './components/SignDetailPage';
|
||||
import SignsPage from './components/SignsPage';
|
||||
import CategoriesPage from './components/CategoryPage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<RandomSignUpload />} />
|
||||
<Route path="/admin/" element={<ProtectedRoute><SignsPage /></ProtectedRoute>} />
|
||||
<Route path="/admin/" element={<ProtectedRoute><CategoriesPage /></ProtectedRoute>} />
|
||||
<Route path="/admin/categories/:id" element={<ProtectedRoute><SignsPage /></ProtectedRoute>} />
|
||||
<Route path="/admin/login" element={<Login />} />
|
||||
<Route path="/admin/signs/:id" element={<ProtectedRoute><SignDetailpage /></ProtectedRoute>} />
|
||||
<Route path="/admin/categories/:category_id/signs/:id" element={<ProtectedRoute><SignDetailpage /></ProtectedRoute>} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
||||
98
frontend/src/components/CategorieComponent.tsx
Executable file
98
frontend/src/components/CategorieComponent.tsx
Executable file
@@ -0,0 +1,98 @@
|
||||
import React, { MouseEventHandler, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Category } from '../types/category';
|
||||
import { Sign } from '../types/sign';
|
||||
import { EditText, EditTextarea, EditTextProps } from 'react-edit-text';
|
||||
import 'react-edit-text/dist/index.css';
|
||||
import { deleteCategory, updateCategory } from '../services/category';
|
||||
|
||||
interface Props {
|
||||
category: Category;
|
||||
deleteCategory: () => void;
|
||||
}
|
||||
|
||||
const SignComponent: React.FC<Props> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const [category, setCategory] = useState<Category>(props.category);
|
||||
const [name, setName] = useState<string>(category.name);
|
||||
|
||||
const onClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
navigate(`/admin/categories/${category.id}/`);
|
||||
};
|
||||
|
||||
const handleSave = (p: any) => {
|
||||
updateCategory({ ...category, "name": p.value })
|
||||
.then((response) => {
|
||||
setCategory({
|
||||
...category,
|
||||
name: p.value
|
||||
});
|
||||
setName(p.value);
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
setName(category.name);
|
||||
alert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleEnableSwitch = (e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
updateCategory({ ...category, "enabled": !category.enabled })
|
||||
.then((response) => {
|
||||
setCategory({
|
||||
...category,
|
||||
enabled: !category.enabled
|
||||
});
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
alert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.deleteCategory();
|
||||
};
|
||||
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, setFn: { (value: React.SetStateAction<string>): void; (arg0: any): void; }) => {
|
||||
setFn(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative bg-white p-6 rounded-lg shadow-md" onClick={onClick}>
|
||||
<button className="absolute top-2 right-2" onClick={handleDeleteClick}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<EditText className="text-2xl font-bold" value={name} onChange={(e) => handleChange(e, setName)} onSave={handleSave} />
|
||||
</div>
|
||||
<div className='mt-4 flex'>
|
||||
<h2>Publically:</h2>
|
||||
<label className="ml-4 inline-flex relative items-center mr-5 cursor-pointer" onClick={handleEnableSwitch}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={category.enabled}
|
||||
readOnly
|
||||
/>
|
||||
<div
|
||||
className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-green-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignComponent;
|
||||
76
frontend/src/components/CategoryPage.tsx
Executable file
76
frontend/src/components/CategoryPage.tsx
Executable file
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getCategories, addCategory, deleteCategory } from '../services/category';
|
||||
import { Category } from '../types/category';
|
||||
import { Sign } from '../types/sign';
|
||||
import SignComponent from './SignComponent';
|
||||
import CategorieComponent from './CategorieComponent';
|
||||
|
||||
|
||||
const CategoriesPage: React.FC = () => {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [newCategory, setNewCategory] = useState<string>('');
|
||||
const [newCategoryError, setNewCategoryError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// get the signs from the api
|
||||
getCategories().then((categories) => {
|
||||
setCategories(categories);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handle_add_category = () => {
|
||||
addCategory(newCategory).then((category) => {
|
||||
setCategories([...categories, category]);
|
||||
setNewCategory('');
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
setNewCategoryError(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteCategory = (category: Category) => {
|
||||
deleteCategory(category.id).then(() => {
|
||||
setCategories(categories.filter((c) => c.id !== category.id));
|
||||
}
|
||||
).catch((error) => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-left bg-gray-100 min-h-screen">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<div className="mb-4">
|
||||
<input
|
||||
className="border border-gray-400 p-2 rounded-lg w-full"
|
||||
type="text"
|
||||
value={newCategory}
|
||||
onChange={(event) => setNewCategory(event.target.value)}
|
||||
placeholder="Enter Category Name"
|
||||
/>
|
||||
</div>
|
||||
<button className="bg-indigo-500 text-white py-2 px-4 rounded-lg hover:bg-indigo-600" onClick={handle_add_category}>
|
||||
Add Category
|
||||
</button>
|
||||
</div>
|
||||
{newCategoryError && <p className="text-red-500">{newCategoryError}</p>}
|
||||
|
||||
<h1 className="text-3xl font-bold mt-5">Categories</h1>
|
||||
{!loading ?
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-5 mt-5">
|
||||
{
|
||||
categories.map((category) => <CategorieComponent key={category.id} category={category} deleteCategory={() => handleDeleteCategory(category)} />)
|
||||
}
|
||||
</div>
|
||||
|
||||
:
|
||||
<p>Loading...</p>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesPage;
|
||||
@@ -14,7 +14,7 @@ const SignComponent: React.FC<Props> = ({ sign, deleteSign }) => {
|
||||
|
||||
const onClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
navigate(`/admin/signs/${sign.id}`);
|
||||
navigate(`/admin/categories/${sign.category_id}/signs/${sign.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
|
||||
@@ -148,7 +148,7 @@ const SignDetailpage: React.FC<Props> = (props) => {
|
||||
}
|
||||
<div>
|
||||
<button onClick={() => {
|
||||
window.location.href = '/admin/';
|
||||
window.location.href = `/admin/categories/${sign!.category_id}`;
|
||||
}} className="bg-white p-2 rounded-full text-red-600 hover:bg-red-600 hover:text-white absolute top-1 left-1">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getSigns, addSign, downloadSigns, deleteSign } from '../services/signs';
|
||||
import { Category } from '../types/category';
|
||||
import { Sign } from '../types/sign';
|
||||
import SignComponent from './SignComponent';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const SignsPage: React.FC = () => {
|
||||
|
||||
const [signs, setSigns] = useState<Sign[]>([]);
|
||||
const [newSign, setNewSign] = useState('');
|
||||
const [newSignError, setNewSignError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const { id } = useParams<Params>();
|
||||
|
||||
const handleAddSign = async () => {
|
||||
addSign(newSign).then((sign) => {
|
||||
addSign(newSign, id!).then((sign) => {
|
||||
setSigns([...signs, sign]);
|
||||
setNewSign('');
|
||||
setNewSignError(null);
|
||||
@@ -44,8 +52,7 @@ const SignsPage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// get the signs from the api
|
||||
getSigns().then((signs) => {
|
||||
console.log(signs)
|
||||
getSigns(id!).then((signs) => {
|
||||
setSigns(signs);
|
||||
setLoading(false);
|
||||
});
|
||||
@@ -63,6 +70,15 @@ const SignsPage: React.FC = () => {
|
||||
placeholder="Enter sign url"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => {
|
||||
window.location.href = `/admin/`;
|
||||
}} className="bg-white p-2 rounded-full text-red-600 hover:bg-red-600 hover:text-white absolute top-1 left-1">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button className="bg-indigo-500 text-white py-2 px-4 rounded-lg hover:bg-indigo-600" onClick={handleAddSign}>
|
||||
Add Sign
|
||||
</button>
|
||||
@@ -70,8 +86,12 @@ const SignsPage: React.FC = () => {
|
||||
Download Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{newSignError && <p className="text-red-500">{newSignError}</p>}
|
||||
|
||||
<h1 className="text-3xl font-bold mt-5">Signs</h1>
|
||||
|
||||
|
||||
{!loading ?
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-5 mt-5">
|
||||
{
|
||||
|
||||
85
frontend/src/services/category.ts
Executable file
85
frontend/src/services/category.ts
Executable file
@@ -0,0 +1,85 @@
|
||||
import { Category } from "../types/category";
|
||||
|
||||
const getCategories = async () => {
|
||||
// get access token from local storage
|
||||
const token = localStorage.getItem('accessToken');
|
||||
// make request to get signs
|
||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/categories/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
// return the response
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const addCategory = async (category: string) => {
|
||||
// get access token from local storage
|
||||
const token = localStorage.getItem('accessToken');
|
||||
// make request to get signs
|
||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/categories/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ "name": category })
|
||||
});
|
||||
|
||||
// check if error
|
||||
if (response.status !== 201) {
|
||||
throw new Error('Error adding category');
|
||||
}
|
||||
|
||||
// return the response
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const updateCategory = async (category: Category) => {
|
||||
// get access token from local storage
|
||||
const token = localStorage.getItem('accessToken');
|
||||
// make request to get signs
|
||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/categories/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(category)
|
||||
});
|
||||
|
||||
// check if error
|
||||
if (response.status !== 200) {
|
||||
throw new Error('Error updating category');
|
||||
}
|
||||
|
||||
// return the response
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const deleteCategory = async (id: number) => {
|
||||
// get access token from local storage
|
||||
const token = localStorage.getItem('accessToken');
|
||||
// make request to get signs
|
||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/categories/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// check if error
|
||||
if (response.status !== 200) {
|
||||
throw new Error('Category must be empty before deleting');
|
||||
}
|
||||
|
||||
// return the response
|
||||
return response.json();
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export { getCategories, addCategory, updateCategory, deleteCategory };
|
||||
@@ -1,8 +1,8 @@
|
||||
const getSigns = async () => {
|
||||
const getSigns = async (category: string) => {
|
||||
// get access token from local storage
|
||||
const token = localStorage.getItem('accessToken');
|
||||
// make request to get signs
|
||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/signs/`, {
|
||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/categories/${category}/signs/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const getSigns = async () => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const addSign = async (url: string) => {
|
||||
const addSign = async (url: string, category: string) => {
|
||||
// get access token from local storage
|
||||
const token = localStorage.getItem('accessToken');
|
||||
// make request to add sign
|
||||
@@ -22,7 +22,7 @@ const addSign = async (url: string) => {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ "url": url })
|
||||
body: JSON.stringify({ "url": url, "category": category })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
7
frontend/src/types/category.ts
Executable file
7
frontend/src/types/category.ts
Executable file
@@ -0,0 +1,7 @@
|
||||
import { Sign } from "./sign";
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export interface Sign {
|
||||
name: string;
|
||||
sign_id: string;
|
||||
video_url: string;
|
||||
category_id: number;
|
||||
sign_videos: [SignVideo];
|
||||
}
|
||||
|
||||
@@ -16,6 +17,7 @@ export interface SimpleSign {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
category_id: number;
|
||||
sign_id: string;
|
||||
video_url: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user