Updating to support categories

This commit is contained in:
2023-03-11 12:32:41 +00:00
parent bdab151dae
commit ec7bd6dde5
27 changed files with 807 additions and 42 deletions

View File

@@ -5,6 +5,8 @@
"dependencies": {
"@ffmpeg/core": "^0.11.0",
"@ffmpeg/ffmpeg": "^0.11.6",
"@heroicons/react": "^2.0.16",
"@styled-icons/heroicons-solid": "^10.47.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
@@ -13,6 +15,7 @@
"@types/node": "^16.7.13",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/react-edit-text": "^5.0.1",
"@types/react-modal": "^3.13.1",
"@types/react-transition-group": "^4.4.5",
"axios": "^1.2.2",
@@ -22,6 +25,7 @@
"fluent-ffmpeg": "^2.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-edit-text": "^5.0.2",
"react-media-recorder": "^1.6.6",
"react-modal": "^3.16.1",
"react-native-trimmer": "^1.1.1",

View File

@@ -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>
);

View 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;

View 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;

View File

@@ -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>) => {

View File

@@ -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" />

View File

@@ -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">
{

View 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 };

View File

@@ -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
View File

@@ -0,0 +1,7 @@
import { Sign } from "./sign";
export interface Category {
id: number;
name: string;
enabled: boolean;
}

View File

@@ -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;
}

View File

@@ -1036,6 +1036,13 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.19.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@@ -1238,6 +1245,11 @@
regenerator-runtime "^0.13.7"
resolve-url "^0.2.1"
"@heroicons/react@^2.0.16":
version "2.0.16"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.16.tgz#562883c19ba2690c83380b42a9a5cce39dcbdb4a"
integrity sha512-x89rFxH3SRdYaA+JCXwfe+RkE1SFTo9GcOkZettHer71Y3T7V+ogKmfw5CjTazgS3d0ClJ7p1NA+SP7VQLQcLw==
"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -1671,6 +1683,21 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@styled-icons/heroicons-solid@^10.47.0":
version "10.47.0"
resolved "https://registry.yarnpkg.com/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz#4457463fe15c8bf8c357bf22dd16d3e579a5e163"
integrity sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==
dependencies:
"@babel/runtime" "^7.20.7"
"@styled-icons/styled-icon" "^10.7.0"
"@styled-icons/styled-icon@^10.7.0":
version "10.7.0"
resolved "https://registry.yarnpkg.com/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz#d6960e719b8567c8d0d3a87c40fb6f5b4952a228"
integrity sha512-SCrhCfRyoY8DY7gUkpz+B0RqUg/n1Zaqrr2+YKmK/AyeNfCcoHuP4R9N4H0p/NA1l7PTU10ZkAWSLi68phnAjw==
dependencies:
"@babel/runtime" "^7.19.0"
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -2088,6 +2115,13 @@
dependencies:
"@types/react" "*"
"@types/react-edit-text@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/react-edit-text/-/react-edit-text-5.0.1.tgz#2907c740c013c17707eb76d1b84688417dff4c27"
integrity sha512-Eb2tX+PtSsbSTZPO1mTbTufVVAGddeplMCHsL+RCXQytm3X8bkiKWXMbQ/kaoOuxDHTkarItHRX0dEeo5A2B+w==
dependencies:
"@types/react" "*"
"@types/react-modal@^3.13.1":
version "3.13.1"
resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.13.1.tgz#5b9845c205fccc85d9a77966b6e16dc70a60825a"
@@ -3234,7 +3268,7 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
classnames@^2.2.1:
classnames@^2.2.1, classnames@^2.2.6:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
@@ -8035,6 +8069,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-edit-text@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/react-edit-text/-/react-edit-text-5.0.2.tgz#e1418a16f615cc5b833f455ccffd24d18c566ab2"
integrity sha512-V3M4KhgQDbKa1I6F5OLrhqu9w/T4XONFEyC4RijdSxDs+L5srpsCH6V5CSWGNT6pu6f4VG03n736RB3lMCTUkw==
dependencies:
classnames "^2.2.6"
prop-types "^15.8.1"
react-error-overlay@^6.0.11:
version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"