diff --git a/admin-frontend/src/App.js b/admin-frontend/src/App.js index 09f16c1daf91766bd8fe3dcd3b7a196b11d1d6a0..b4100f036ac35f06c173f51be4cf2981ad7a0bed 100644 --- a/admin-frontend/src/App.js +++ b/admin-frontend/src/App.js @@ -11,6 +11,7 @@ import { ScraperList } from './scraperList'; import { orderList } from './questionOrderList'; import { archiveList } from './archiveList'; import { criteriaList } from './criteriaList'; +import { snackList, snackEdit, snackCreate } from './snackList'; import RestoreFromTrashIcon from '@material-ui/icons/RestoreFromTrash'; import SportsKabaddiIcon from '@material-ui/icons/SportsKabaddi'; @@ -19,6 +20,8 @@ import SportsFootballIcon from '@material-ui/icons/SportsFootball'; import ListAltIcon from '@material-ui/icons/ListAlt'; import YoutubeSearchedForIcon from '@material-ui/icons/YoutubeSearchedFor'; import FormatListNumberedIcon from '@material-ui/icons/FormatListNumbered'; +import RestaurantIcon from '@material-ui/icons/Restaurant'; +import AnnouncementIcon from '@material-ui/icons/Announcement'; const App = () => ( @@ -88,6 +91,28 @@ const App = () => ( options={{ label: 'Kriterienübersicht', "menuParent": "fragen" }} list={criteriaList} /> + + <Resource name='snacks_n_co' options={{ "label": "Snacks & Co.", "isMenuParent": true }} /> + + <Resource + name='snack' + icon={RestaurantIcon} + options={{ label: 'Wissenssnacks', "menuParent": "snacks_n_co" }} + list={snackList} + edit={snackEdit} + create={snackCreate} + /> + + <Resource + name='activity' + icon={AnnouncementIcon} + options={{ label: 'Aktivitäten', "menuParent": "snacks_n_co" }} + list={snackList} + edit={snackEdit} + create={snackCreate} + /> + + </Admin> ); diff --git a/admin-frontend/src/activitySnackProvider.js b/admin-frontend/src/activitySnackProvider.js new file mode 100644 index 0000000000000000000000000000000000000000..fd0f702d3699913c1172fe3a93ed9f3871f7efb3 --- /dev/null +++ b/admin-frontend/src/activitySnackProvider.js @@ -0,0 +1,196 @@ +import { stringify } from 'query-string'; +import { + fetchUtils, +} from 'ra-core'; +import dataProviderMapper from './dataProviderMapper'; + +/* +DEFAULT DATA PROVIDER TEMPLATE +Just for copy and pasting when other data providers are needed. +*/ + + +// export { +// default as tokenAuthProvider, +// fetchJsonWithAuthToken, +// } from './tokenAuthProvider'; + +// export { +// default as jwtTokenAuthProvider, +// fetchJsonWithAuthJWTToken, +// } from './jwtTokenAuthProvider'; + +const getPaginationQuery = (pagination) => { + return { + page: pagination.page, + page_size: pagination.perPage, + }; +}; + +const getFilterQuery = (filter) => { + const { q: search, ...otherSearchParams } = filter; + return { + ...otherSearchParams, + search, + }; +}; + +export const getOrderingQuery = (sort) => { + const { field, order } = sort; + return { + ordering: `${order === 'ASC' ? '' : '-'}${field}`, + }; +}; + +export default ( + apiUrl, + httpClient = fetchUtils.fetchJson +) => { + const getOneJson = (resource, id) => + httpClient(`${apiUrl}/${resource}/${id}/`).then( + (response) => response.json + ); + + return { + getList: async (resource, params) => { + const query = { + ...getFilterQuery(params.filter), + ...getPaginationQuery(params.pagination), + ...getOrderingQuery(params.sort), + }; + const url = `${apiUrl}/${resource}/?${stringify(query)}`; + + const { json } = await httpClient(url); + + return { + data: json.results, + total: json.count, + }; + }, + + getOne: async (resource, params) => { + const data = await getOneJson(resource, params.id); + return { + data + }; + }, + + getMany: (resource, params) => { + return Promise.all( + params.ids.map(id => getOneJson(resource, id)) + ).then(data => ({ data })); + }, + + getManyReference: async (resource, params) => { + const query = { + ...getFilterQuery(params.filter), + ...getPaginationQuery(params.pagination), + ...getOrderingQuery(params.sort), + [params.target]: params.id, + }; + const url = `${apiUrl}/${resource}/?${stringify(query)}`; + + const { json } = await httpClient(url); + return { + data: json.results, + total: json.count, + }; + }, + + update: async (resource, params) => { + + + let data = { + text_de: params.data.text_de, + text_en: params.data.text_en, + } + + if (params.data.new_image !== undefined) { + + let image = params.data.new_image; + let image64 = await convertFileToBase64(image); + + let new_image_type = params.data.new_image.rawFile.type; + + data = { + ...data, + image_type: new_image_type, + image: image64 + } + } + + const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}/`, { + method: 'PATCH', + body: JSON.stringify( + data + ), + }); + return { data: json }; + }, + + updateMany: (resource, params) => + Promise.all( + params.ids.map(id => + httpClient(`${apiUrl}/${resource}/${id}/`, { + method: 'PATCH', + body: JSON.stringify(params.data), + }) + ) + ).then(responses => ({ data: responses.map(({ json }) => json.id) })), + + create: async (resource, params) => { + + + let data = { + text_de: params.data.text_de, + text_en: params.data.text_en, + } + + if (params.data.new_image !== undefined) { + + let image = params.data.new_image; + let image64 = await convertFileToBase64(image); + + let new_image_type = params.data.new_image.rawFile.type; + + data = { + ...data, + image_type: new_image_type, + image: image64 + } + } + + const { json } = await httpClient(`${apiUrl}/${resource}/`, { + method: 'POST', + body: JSON.stringify(data), + }); + return { + data: { ...json }, + }; + }, + + delete: (resource, params) => + httpClient(`${apiUrl}/${resource}/${params.id}/`, { + method: 'DELETE', + }).then(() => ({ data: params.previousData })), + + deleteMany: (resource, params) => + Promise.all( + params.ids.map(id => + httpClient(`${apiUrl}/${resource}/${id}/`, { + method: 'DELETE', + }) + ) + ).then(responses => ({ data: responses.map(({ json }) => json.id) })), + }; +}; + + +const convertFileToBase64 = file => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + + reader.readAsDataURL(file.rawFile); + }); \ No newline at end of file diff --git a/admin-frontend/src/dataProviderMapper.js b/admin-frontend/src/dataProviderMapper.js index 7f549895df755439380e0fc387a18cda965f95e7..161713f41a84c2dcef3e55b21795c349c5f6fade 100644 --- a/admin-frontend/src/dataProviderMapper.js +++ b/admin-frontend/src/dataProviderMapper.js @@ -2,6 +2,7 @@ import drfProvider from 'ra-data-django-rest-framework'; import sportIncompleteProvider from './sportIncompleteProvider.js'; import scraperDataProvider from './scraperDataProvider.js'; import questionOrderProvider from './questionOrderProvider.js'; +import activitySnackProvider from './activitySnackProvider.js'; import { GET_LIST, GET_ONE, @@ -33,6 +34,10 @@ const dataProviders = [ { dataProvider: questionOrderProvider('http://localhost:8000/api/admin'), resources: ['question-order'], + }, + { + dataProvider: activitySnackProvider('http://localhost:8000/api/admin'), + resources: ['snack', 'activity'], } ]; diff --git a/admin-frontend/src/snackList.js b/admin-frontend/src/snackList.js new file mode 100644 index 0000000000000000000000000000000000000000..4b50784de4ea7e033d8af4be3a7e8291cd2f38f8 --- /dev/null +++ b/admin-frontend/src/snackList.js @@ -0,0 +1,196 @@ +import * as React from 'react'; +import { + List, + Datagrid, + TextField, + Edit, + SimpleForm, + BooleanField, + TextInput, + ImageInput, + ImageField, + Create, + required +} from 'react-admin'; + +import Typography from '@material-ui/core/Typography' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' + +const AsideSnackList = () => ( + <div style={{ width: 400, margin: '1em' }}> + <Card> + <CardContent> + <Typography + variant="h4" + align="center" + > + Tipps&Tricks + </Typography> + <br /> + <ul> + <li>Klicke auf eine Datenreihe, um sie zu bearbeiten.</li> + <br /> + <li>Wissenssnacks/Aktivitäten müssen eine deutsche Übersetzung und können eine englische Übersetzung haben.</li> + <br /> + </ul> + + </CardContent> + </Card> + </div> +); + +const AsideSnackEdit = () => ( + <div style={{ width: 400, margin: '1em' }}> + <Card> + <CardContent> + <Typography + variant="h4" + align="center" + > + Tipps&Tricks + </Typography> + <br /> + <ul> + <li>Klicke auf eine Datenreihe, um sie zu bearbeiten.</li> + <br /> + <li>Wissenssnacks/Aktivitäten müssen eine deutsche Übersetzung und können eine englische Übersetzung haben.</li> + <br /> + <li>Es wird das Bild angezeigt, was momentan im Quiz benutzt wird. Wenn ein neues Bild hochgeladen wird, so wird das Alte überschrieben.</li> + <br /> + </ul> + + </CardContent> + </Card> + </div> +); + +const AsideSnackCreate = () => ( + <div style={{ width: 400, margin: '1em' }}> + <Card> + <CardContent> + <Typography + variant="h4" + align="center" + > + Tipps&Tricks + </Typography> + <br /> + <ul> + <li>Klicke auf eine Datenreihe, um sie zu bearbeiten.</li> + <br /> + <li>Wissenssnacks/Aktivitäten müssen eine deutsche Übersetzung und können eine englische Übersetzung haben.</li> + <br /> + <li>Es kann außerdem das Bild hochgeladen werden, was im Quiz für diesen Snack/diese Aktivität angezeigt werden soll.</li> + <br /> + </ul> + + </CardContent> + </Card> + </div> +); + + +export const snackList = props => ( + <List {...props} + mutationMode={"pessimistic"} + aside={<AsideSnackList />} + > + <Datagrid rowClick="edit"> + <TextField + source="id" + label="Id" + sortable={false} + /> + <TextField + source="text_de" + label="Deutscher Text" + sortable={false} + /> + <TextField + source="text_en" + label="Englischer Text" + sortable={false} + /> + <BooleanField + source="has_image" + label="Hat Bild" + sortable={false} + /> + </Datagrid> + </List> +); + +export const snackEdit = props => ( + <Edit {...props} + mutationMode={"pessimistic"} + aside={<AsideSnackEdit />} + > + <SimpleForm> + <TextField + source="id" + label="ID" + /> + <TextInput + source="text_de" + label="Deutsche Übersetzung" + validate={[required("Deutsche Übersetzung ist benötigt")]} + fullWidth + /> + <TextInput + source="text_en" + label="Englische Übersetzung" + fullWidth + + /> + <ImageField + source="image_url" + label="Momentanes Bild" + /> + <ImageInput + source="new_image" + accept="image/*" + label="Bild"> + + <ImageField + source="src" + title="Ausgewähltes Bild" + /> + </ImageInput> + </SimpleForm> + </Edit> +); + + +export const snackCreate = props => ( + <Create {...props} + mutationMode={"pessimistic"} + aside={<AsideSnackCreate />} + > + <SimpleForm + redirect="list" + > + <TextInput + source="text_de" + label="Deutsche Übersetzung" + fullWidth + validate={[required("Deutsche Übersetzung ist benötigt")]} + /> + <TextInput + source="text_en" + label="Englische Übersetzung" + fullWidth + /> + <ImageInput + source="new_image" + accept="image/*" + label="Bild" + > + <ImageField + source="src" + title="Ausgewähltes Bild" + /> + </ImageInput> + </SimpleForm> + </Create> +); \ No newline at end of file