Starting a UI refactor.

pull/7/head
Sander Vocke 5 years ago
parent e2a4d8bc24
commit fba3d73484
  1. 1
      client/package.json
  2. 107
      client/src/App.tsx
  3. 101
      client/src/components/AppBar.tsx
  4. 24
      client/src/components/ItemList.tsx
  5. 15
      client/src/components/ItemListItem.tsx
  6. 30
      client/src/components/ItemListLoadedSongItem.tsx
  7. 22
      client/src/components/ItemListLoadingSongItem.tsx
  8. 18
      client/src/components/ItemListSongItem.tsx
  9. 18
      client/src/types/DisplayItem.tsx
  10. 7
      client/yarn.lock

@ -4,6 +4,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.56", "@material-ui/lab": "^4.0.0-alpha.56",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",

@ -1,12 +1,23 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Box, Button } from '@material-ui/core'; import { Box, Button, Paper } from '@material-ui/core';
import * as serverApi from './api'; import * as serverApi from './api';
import SongTable, { Entry as SongEntry } from './components/SongTable';
import ArtistTable, { Entry as ArtistEntry } from './components/ArtistTable';
import EditSongDialog, { SongProperties } from './components/EditSongDialog'; import EditSongDialog, { SongProperties } from './components/EditSongDialog';
import EditArtistDialog, { ArtistProperties } from './components/EditArtistDialog'; import EditArtistDialog, { ArtistProperties } from './components/EditArtistDialog';
import AppBar from './components/AppBar';
import ItemList from './components/ItemList';
import ItemListItem from './components/ItemListItem';
import { SongDisplayItem, LoadingSongDisplayItem } from './types/DisplayItem';
interface SongEntry {
}
interface ArtistEntry {
name: String,
id: Number,
}
function App() { function App() {
const [songs, setSongs] = useState<SongEntry[]>([]); const [songs, setSongs] = useState<SongEntry[]>([]);
@ -22,61 +33,61 @@ function App() {
}); });
const updateSongs = () => { const updateSongs = () => {
return fetch(serverApi.ListSongsEndpoint) return fetch(serverApi.QuerySongsEndpoint)
.then((response: any) => response.json()) // .then((response: any) => response.json())
.then((result: serverApi.ListSongsResponse) => { // .then((result: serverApi.ListSongsResponse) => {
setSongs(result.map((item: serverApi.ListSongsResponseItem) => { // setSongs(result.map((item: serverApi.ListSongsResponseItem) => {
return { // return {
title: item.title, // title: item.title,
artistName: item.artistName // artistName: item.artistName
}; // };
})); // }));
}); // });
} }
const fetchArtists = () => { const fetchArtists = () => {
return fetch(serverApi.ListArtistsEndpoint) return fetch(serverApi.QueryArtistsEndpoint)
.then((response: any) => response.json()) // .then((response: any) => response.json())
.then((result: serverApi.ListArtistsResponse) => { // .then((result: serverApi.ListArtistsResponse) => {
return result.map((item: serverApi.ListArtistsResponseItem) => { // return result.map((item: serverApi.ListArtistsResponseItem) => {
return { // return {
name: item.name, // name: item.name,
id: item.id, // id: item.id,
}; // };
}); // });
}); // });
} }
const updateArtists = () => { const updateArtists = () => {
return fetchArtists() return fetchArtists()
.then((artists: any[]) => { // .then((artists: any[]) => {
setArtists(artists); // setArtists(artists);
}); // });
} }
const createSong = (p:SongProperties) => { const createSong = (p: SongProperties) => {
if(!p.artistId) { if (!p.artistId) {
throw "Undefined artist ID for song to be created."; throw "Undefined artist ID for song to be created.";
} }
const request:serverApi.CreateSongRequest = { const request: serverApi.CreateSongRequest = {
title: p.title, title: p.title,
artistId: p.artistId, artistIds: [p.artistId],
} }
const requestOpts = { const requestOpts = {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request) body: JSON.stringify(request)
}; };
return fetch(serverApi.CreateSongEndpoint, requestOpts) return fetch(serverApi.CreateSongEndpoint, requestOpts)
} }
const createArtist = (p:ArtistProperties) => { const createArtist = (p: ArtistProperties) => {
const request:serverApi.CreateArtistRequest = { const request: serverApi.CreateArtistRequest = {
name: p.name, name: p.name,
} }
const requestOpts = { const requestOpts = {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request) body: JSON.stringify(request)
}; };
return fetch(serverApi.CreateArtistEndpoint, requestOpts) return fetch(serverApi.CreateArtistEndpoint, requestOpts)
@ -112,22 +123,30 @@ function App() {
useEffect(() => { updateSongs(); }, []); useEffect(() => { updateSongs(); }, []);
useEffect(() => { updateArtists(); }, []); useEffect(() => { updateArtists(); }, []);
const testSong: SongDisplayItem = {
title: "My Song",
artistNames: ["First dude", "Second dude"]
};
const testLoadingSong: LoadingSongDisplayItem = {
loadingSong: true
};
return ( return (
<div style={{ maxWidth: '100%' }}> <div style={{ maxWidth: '100%' }}>
<AppBar />
<Paper>
<ItemList items={[testSong, testLoadingSong]}>
<ItemListItem item={testSong}/>
<ItemListItem item={testLoadingSong}/>
</ItemList>
</Paper>
<Button variant="contained" onClick={openCreateSongDialog}>Create Song</Button> <Button variant="contained" onClick={openCreateSongDialog}>Create Song</Button>
<Button variant="contained" onClick={openCreateArtistDialog}>Create Artist</Button> <Button variant="contained" onClick={openCreateArtistDialog}>Create Artist</Button>
<Box>
<SongTable
songs={songs}
/>
<ArtistTable
artists={artists}
/>
</Box>
<EditSongDialog <EditSongDialog
dialogOpen={editSongDialogOpen} dialogOpen={editSongDialogOpen}
onClose={() => { setEditSongDialogOpen(false); }} onClose={() => { setEditSongDialogOpen(false); }}
onChangeSongProperties={(props:SongProperties) => setSongDialogProperties(props)} onChangeSongProperties={(props: SongProperties) => setSongDialogProperties(props)}
songProperties={songDialogProperties} songProperties={songDialogProperties}
onSubmit={onSubmitCreateSongDialog} onSubmit={onSubmitCreateSongDialog}
artists={artists} artists={artists}
@ -135,7 +154,7 @@ function App() {
<EditArtistDialog <EditArtistDialog
dialogOpen={editArtistDialogOpen} dialogOpen={editArtistDialogOpen}
onClose={() => { setEditArtistDialogOpen(false); }} onClose={() => { setEditArtistDialogOpen(false); }}
onChangeArtistProperties={(props:ArtistProperties) => setArtistDialogProperties(props)} onChangeArtistProperties={(props: ArtistProperties) => setArtistDialogProperties(props)}
artistProperties={artistDialogProperties} artistProperties={artistDialogProperties}
onSubmit={onSubmitCreateArtistDialog} onSubmit={onSubmitCreateArtistDialog}
/> />

@ -0,0 +1,101 @@
import React from 'react';
import MuiAppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import InputBase from '@material-ui/core/InputBase';
import { createStyles, fade, Theme, makeStyles } from '@material-ui/core/styles';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
}),
);
export default function AppBar() {
const classes = useStyles();
return (
<div className={classes.root}>
<MuiAppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="open drawer"
>
<MenuIcon />
</IconButton>
<Typography className={classes.title} variant="h6" noWrap>MuDBase</Typography>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
inputProps={{ 'aria-label': 'search' }}
/>
</div>
</Toolbar>
</MuiAppBar>
</div>
);
}

@ -0,0 +1,24 @@
import React from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
maxWidth: 752,
},
}),
);
export default function ItemList(props:any) {
const classes = useStyles();
return (
<div className={classes.root}>
<List dense={true}>
{props.children}
</List>
</div>
);
}

@ -0,0 +1,15 @@
import React from 'react';
import { DisplayItem, isSong, isLoadingSong } from '../types/DisplayItem';
import ItemListLoadedSongItem from './ItemListLoadedSongItem';
import ItemListLoadingSongItem from './ItemListLoadingSongItem';
export interface IProps {
item: DisplayItem
}
export default function ItemListItem(props: IProps) {
return <>
{isSong(props.item) && <ItemListLoadedSongItem item={props.item}/>}
{isLoadingSong(props.item) && <ItemListLoadingSongItem item={props.item}/>}
</>
}

@ -0,0 +1,30 @@
import React from 'react';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import MusicNoteIcon from '@material-ui/icons/MusicNote';
import { SongDisplayItem } from '../types/DisplayItem';
export interface IProps {
item: SongDisplayItem
}
export default function ItemListLoadedSongItem(props: IProps) {
var artists = props.item.artistNames.length ? props.item.artistNames[0] : "Unknown";
for(var i:number=1; i<props.item.artistNames.length; i++) {
artists = artists.concat(", " + props.item.artistNames[i]);
}
return (
<ListItem>
<ListItemIcon>
<MusicNoteIcon />
</ListItemIcon>
<ListItemText
primary={props.item.title}
secondary={artists}
/>
</ListItem>
);
}

@ -0,0 +1,22 @@
import React from 'react';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import MusicNoteIcon from '@material-ui/icons/MusicNote';
import CircularProgress from '@material-ui/core/CircularProgress';
import { LoadingSongDisplayItem } from '../types/DisplayItem';
export interface IProps {
item: LoadingSongDisplayItem
}
export default function ItemListLoadingSongItem(props: IProps) {
return (
<ListItem>
<ListItemIcon>
<MusicNoteIcon />
</ListItemIcon>
<CircularProgress size={24}/>
</ListItem>
);
}

@ -0,0 +1,18 @@
import React, { useEffect } from 'react';
import ItemListItem from './ItemListItem';
import { SongDisplayItem, LoadingSongDisplayItem } from '../types/DisplayItem';
export interface IProps {
getDetails: () => Promise<SongDisplayItem>
}
export default function ItemListSongItem(props: IProps) {
const [ song, setSong ] = React.useState<SongDisplayItem | LoadingSongDisplayItem>({ loadingSong: true });
useEffect(() => {
props.getDetails()
.then((details:SongDisplayItem) => { setSong(details); });
});
return <ItemListItem item={song}/>
}

@ -0,0 +1,18 @@
export interface SongDisplayItem {
title:String,
artistNames:String[],
}
export interface LoadingSongDisplayItem {
loadingSong: boolean,
}
export type DisplayItem = SongDisplayItem | LoadingSongDisplayItem;
export function isSong(item: DisplayItem): item is SongDisplayItem {
return "title" in item;
}
export function isLoadingSong(item: DisplayItem): item is LoadingSongDisplayItem {
return "loadingSong" in item;
}

@ -1321,6 +1321,13 @@
react-is "^16.8.0" react-is "^16.8.0"
react-transition-group "^4.4.0" react-transition-group "^4.4.0"
"@material-ui/icons@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665"
integrity sha512-GBitL3oBWO0hzBhvA9KxqcowRUsA0qzwKkURyC8nppnC3fw54KPKZ+d4V1Eeg/UnDRSzDaI9nGCdel/eh9AQMg==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/lab@^4.0.0-alpha.56": "@material-ui/lab@^4.0.0-alpha.56":
version "4.0.0-alpha.56" version "4.0.0-alpha.56"
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz#ff63080949b55b40625e056bbda05e130d216d34" resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz#ff63080949b55b40625e056bbda05e130d216d34"

Loading…
Cancel
Save