@@ -15,7 +15,7 @@ | |||
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone from the back-end. | |||
- [ ] In the back-end, the server sends the ringtone data to the front-end. | |||
- As a client, I want to browse ringtones. | |||
- [ ] In the front-end, the client provides optional skip and take arguments to request multiple ringtones from the | |||
- [X] In the front-end, the client provides optional skip and take arguments to request multiple ringtones from the | |||
back-end. | |||
- [X] In the back-end, the server sends the ringtones to the front-end. | |||
- As a client, I want to view the ringtones made by a composer. | |||
@@ -18,6 +18,7 @@ const Base = styled('div')({ | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
opacity: 0.5, | |||
}, | |||
}) | |||
@@ -18,6 +18,7 @@ const Base = styled('div')({ | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
opacity: 0.5, | |||
}, | |||
}) | |||
@@ -17,6 +17,7 @@ const Base = styled('div')({ | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
opacity: 0.5, | |||
}, | |||
}) | |||
@@ -18,6 +18,7 @@ const Base = styled('div')({ | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
opacity: 0.5, | |||
}, | |||
}) | |||
@@ -19,6 +19,7 @@ const Base = styled('div')({ | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
pointerEvents: 'none', | |||
opacity: 0.5, | |||
}, | |||
}) | |||
@@ -0,0 +1,26 @@ | |||
import styled from 'styled-components'; | |||
import {FC} from 'react'; | |||
const Base = styled('img')({ | |||
objectFit: 'cover', | |||
objectPosition: 'center', | |||
width: '3rem', | |||
height: '3rem', | |||
borderRadius: '50%', | |||
}) | |||
type Props = { | |||
src: string, | |||
} | |||
const Avatar: FC<Props> = ({ | |||
src, | |||
}) => { | |||
return ( | |||
<Base | |||
src={src} | |||
/> | |||
) | |||
} | |||
export default Avatar |
@@ -6,6 +6,20 @@ const Base = styled('div')({ | |||
position: 'relative', | |||
padding: '1rem', | |||
boxSizing: 'border-box', | |||
'::before': { | |||
content: "''", | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '100%', | |||
boxSizing: 'border-box', | |||
borderRadius: 'inherit', | |||
pointerEvents: 'none', | |||
backgroundColor: 'white', | |||
mixBlendMode: 'screen', | |||
opacity: 0.125, | |||
}, | |||
'::after': { | |||
content: "''", | |||
position: 'absolute', | |||
@@ -17,9 +31,14 @@ const Base = styled('div')({ | |||
borderRadius: 'inherit', | |||
pointerEvents: 'none', | |||
border: '1px solid', | |||
opacity: 0.5, | |||
}, | |||
}) | |||
const Content = styled('div')({ | |||
position: 'relative', | |||
}) | |||
type Props = { | |||
className?: string, | |||
} | |||
@@ -32,7 +51,9 @@ const Card: FC<Props> = ({ | |||
<Base | |||
{...etcProps} | |||
> | |||
{children} | |||
<Content> | |||
{children} | |||
</Content> | |||
</Base> | |||
) | |||
} | |||
@@ -0,0 +1,114 @@ | |||
import {LeftSidebarWithMenu} from '@theoryofnekomata/viewfinder'; | |||
import Brand from '../../../molecules/brand/Brand'; | |||
import Link from '../../../molecules/navigation/Link'; | |||
import OmnisearchForm from '../../forms/Omnisearch'; | |||
import {moreLinkMenuItem, sidebarMenuItems} from '../../../../data/layout'; | |||
import {FC, FormEventHandler} from 'react'; | |||
import {Session} from '@auth0/nextjs-auth0'; | |||
import styled from 'styled-components'; | |||
import Avatar from '../../../molecules/presentation/Avatar'; | |||
const TopBarComponent = styled('div')({ | |||
backgroundColor: 'var(--color-bg, white)', | |||
position: 'relative', | |||
'::before': { | |||
content: "''", | |||
position: 'absolute', | |||
bottom: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '0.0625rem', | |||
pointerEvents: 'none', | |||
backgroundColor: 'currentcolor', | |||
opacity: 0.25, | |||
}, | |||
}) | |||
const SidebarMenuComponent = styled('div')({ | |||
backgroundColor: 'var(--color-bg, white)', | |||
'::before': { | |||
content: "''", | |||
position: 'absolute', | |||
top: 0, | |||
right: 0, | |||
height: '100%', | |||
width: '0.0625rem', | |||
pointerEvents: 'none', | |||
backgroundColor: 'currentcolor', | |||
opacity: 0.25, | |||
}, | |||
}) | |||
type Props = { | |||
onSearch?: FormEventHandler, | |||
session?: Partial<Session>, | |||
} | |||
const BasicLayout: FC<Props> = ({ | |||
onSearch, | |||
session, | |||
children, | |||
}) => { | |||
return ( | |||
<LeftSidebarWithMenu.Layout | |||
brand={ | |||
<Brand /> | |||
} | |||
userLink={ | |||
<Link | |||
href={{ | |||
query: { | |||
popup: 'user', | |||
}, | |||
}} | |||
> | |||
<Avatar | |||
src={session.user.picture} | |||
/> | |||
</Link> | |||
} | |||
topBarComponent={TopBarComponent} | |||
sidebarMenuComponent={SidebarMenuComponent} | |||
topBarCenter={ | |||
<OmnisearchForm | |||
labels={{ | |||
form: 'Search', | |||
query: 'Query', | |||
}} | |||
onSubmit={onSearch} | |||
action="/api/a/search" | |||
/> | |||
} | |||
linkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.SidebarMenuContainer> | |||
<LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.SidebarMenuContainer> | |||
</Link> | |||
)} | |||
moreLinkMenuItem={moreLinkMenuItem} | |||
moreLinkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
<LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
</Link> | |||
)} | |||
sidebarMenuItems={sidebarMenuItems} | |||
> | |||
{children} | |||
</LeftSidebarWithMenu.Layout> | |||
) | |||
} | |||
export default BasicLayout |
@@ -1,32 +1,14 @@ | |||
import {FC, FormEventHandler} from 'react'; | |||
import styled from 'styled-components'; | |||
import { LeftSidebarWithMenu } from '@theoryofnekomata/viewfinder' | |||
import Brand from '../../molecules/brand/Brand'; | |||
import Link from '../../molecules/navigation/Link'; | |||
import OmnisearchForm from '../../organisms/forms/Omnisearch'; | |||
import {Session} from '@auth0/nextjs-auth0'; | |||
import {models} from '@tonality/library-common'; | |||
import RingtoneCardDisplay from '../../organisms/presentation/RingtoneCardDisplay'; | |||
import Card from '../../molecules/presentation/Card'; | |||
import {moreLinkMenuItem, sidebarMenuItems} from '../../../data/layout'; | |||
import ActionButton from '../../molecules/forms/ActionButton'; | |||
import NumericInput from '../../molecules/forms/NumericInput'; | |||
const TopBarComponent = styled('div')({ | |||
backgroundColor: 'var(--color-bg, white)', | |||
}) | |||
const SidebarMenuComponent = styled('div')({ | |||
backgroundColor: 'var(--color-bg, white)', | |||
}) | |||
const Avatar = styled('img')({ | |||
objectFit: 'cover', | |||
objectPosition: 'center', | |||
width: '3rem', | |||
height: '3rem', | |||
borderRadius: '50%', | |||
}) | |||
import BasicLayout from '../../organisms/presentation/BasicLayout'; | |||
const CardList = styled('div')({ | |||
display: 'grid', | |||
@@ -96,66 +78,9 @@ const BrowseRingtonesTemplate: FC<Props> = ({ | |||
loading, | |||
}) => { | |||
return ( | |||
<LeftSidebarWithMenu.Layout | |||
brand={ | |||
<Brand /> | |||
} | |||
userLink={ | |||
<Link | |||
href={{ | |||
query: { | |||
popup: 'user', | |||
}, | |||
}} | |||
> | |||
{ | |||
session | |||
&& ( | |||
<Avatar | |||
src={session.user.picture} | |||
/> | |||
) | |||
} | |||
</Link> | |||
} | |||
topBarComponent={TopBarComponent} | |||
sidebarMenuComponent={SidebarMenuComponent} | |||
topBarCenter={ | |||
<OmnisearchForm | |||
labels={{ | |||
form: 'Search', | |||
query: 'Query', | |||
}} | |||
onSubmit={onSearch} | |||
action="/api/a/search" | |||
/> | |||
} | |||
linkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.SidebarMenuContainer> | |||
<LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.SidebarMenuContainer> | |||
</Link> | |||
)} | |||
moreLinkMenuItem={moreLinkMenuItem} | |||
moreLinkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
<LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
</Link> | |||
)} | |||
sidebarMenuItems={sidebarMenuItems} | |||
<BasicLayout | |||
onSearch={onSearch} | |||
session={session} | |||
> | |||
<LeftSidebarWithMenu.ContentContainer> | |||
{ | |||
@@ -230,7 +155,7 @@ const BrowseRingtonesTemplate: FC<Props> = ({ | |||
) | |||
} | |||
</LeftSidebarWithMenu.ContentContainer> | |||
</LeftSidebarWithMenu.Layout> | |||
</BasicLayout> | |||
) | |||
} | |||
@@ -5,10 +5,8 @@ describe('template for creating ringtones', () => { | |||
it('should render without crashing', () => { | |||
expect(() => render( | |||
<CreateRingtoneTemplate | |||
composerRingtones={[]} | |||
composer={{ | |||
name: 'TheoryOfNekomata', | |||
id: '00000000-0000-0000-000000000000', | |||
session={{ | |||
user: {}, | |||
}} | |||
/> | |||
)).not.toThrow() | |||
@@ -1,46 +1,26 @@ | |||
import {FC, FormEventHandler} from 'react' | |||
import styled from 'styled-components' | |||
import {Session} from '@auth0/nextjs-auth0' | |||
import { LeftSidebarWithMenu } from '@theoryofnekomata/viewfinder' | |||
import {models} from '@tonality/library-common' | |||
import CreateRingtoneForm from '../../organisms/forms/CreateRingtone' | |||
import Link from '../../molecules/navigation/Link' | |||
import OmnisearchForm from '../../organisms/forms/Omnisearch' | |||
import Brand from '../../molecules/brand/Brand' | |||
import {moreLinkMenuItem, sidebarMenuItems} from '../../../data/layout'; | |||
const TopBarComponent = styled('div')({ | |||
backgroundColor: 'var(--color-bg, white)', | |||
}) | |||
const SidebarMenuComponent = styled('div')({ | |||
backgroundColor: 'var(--color-bg, white)', | |||
}) | |||
import BasicLayout from '../../organisms/presentation/BasicLayout'; | |||
const Padding = styled('div')({ | |||
margin: '2rem 0', | |||
}) | |||
const Avatar = styled('img')({ | |||
objectFit: 'cover', | |||
objectPosition: 'center', | |||
width: '3rem', | |||
height: '3rem', | |||
borderRadius: '50%', | |||
}) | |||
type Props = { | |||
onSearch?: FormEventHandler, | |||
onSubmit?: FormEventHandler, | |||
session: Partial<Session>, | |||
currentRingtone?: models.Ringtone, | |||
updateTempo: ({ songRef, dataRef, setTempo, }) => (...args: unknown[]) => void, | |||
updateView: ({ setNoteGlyph, setRestGlyph, noteGlyphs, restGlyphs, }) => (...args: unknown[]) => void, | |||
addRest: ({ formRef, dataRef, songRef, }) => (...args: unknown[]) => void, | |||
addNote: ({ dataRef, formRef, songRef, soundManagerRef, }) => (...args: unknown[]) => void, | |||
togglePlayback: ({ formRef, songRef, soundManagerRef, setPlayTimestamp, setPlaying, }) => (...args: unknown[]) => void, | |||
updateSong: ({ formRef, songRef, }) => (...args: unknown[]) => void, | |||
play: ({ dataRef, playing, setPlaying, playTimestamp, }) => void, | |||
updateTempo?: ({ songRef, dataRef, setTempo, }) => (...args: unknown[]) => void, | |||
updateView?: ({ setNoteGlyph, setRestGlyph, noteGlyphs, restGlyphs, }) => (...args: unknown[]) => void, | |||
addRest?: ({ formRef, dataRef, songRef, }) => (...args: unknown[]) => void, | |||
addNote?: ({ dataRef, formRef, songRef, soundManagerRef, }) => (...args: unknown[]) => void, | |||
togglePlayback?: ({ formRef, songRef, soundManagerRef, setPlayTimestamp, setPlaying, }) => (...args: unknown[]) => void, | |||
updateSong?: ({ formRef, songRef, }) => (...args: unknown[]) => void, | |||
play?: ({ dataRef, playing, setPlaying, playTimestamp, }) => void, | |||
} | |||
const CreateRingtoneTemplate: FC<Props> = ({ | |||
@@ -57,61 +37,9 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||
play, | |||
}) => { | |||
return ( | |||
<LeftSidebarWithMenu.Layout | |||
brand={ | |||
<Brand /> | |||
} | |||
userLink={ | |||
<Link | |||
href={{ | |||
query: { | |||
popup: 'user', | |||
}, | |||
}} | |||
> | |||
<Avatar | |||
src={session.user.picture} | |||
/> | |||
</Link> | |||
} | |||
topBarComponent={TopBarComponent} | |||
sidebarMenuComponent={SidebarMenuComponent} | |||
topBarCenter={ | |||
<OmnisearchForm | |||
labels={{ | |||
form: 'Search', | |||
query: 'Query', | |||
}} | |||
onSubmit={onSearch} | |||
action="/api/a/search" | |||
/> | |||
} | |||
linkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.SidebarMenuContainer> | |||
<LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.SidebarMenuContainer> | |||
</Link> | |||
)} | |||
moreLinkMenuItem={moreLinkMenuItem} | |||
moreLinkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
<LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
</Link> | |||
)} | |||
sidebarMenuItems={sidebarMenuItems} | |||
<BasicLayout | |||
onSearch={onSearch} | |||
session={session} | |||
> | |||
<Padding> | |||
<CreateRingtoneForm | |||
@@ -136,7 +64,7 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||
play={play} | |||
/> | |||
</Padding> | |||
</LeftSidebarWithMenu.Layout> | |||
</BasicLayout> | |||
) | |||
} | |||
@@ -2,7 +2,7 @@ export const sidebarMenuItems = [ | |||
{ | |||
id: 'browse', | |||
label: 'Browse', | |||
icon: 'B', | |||
icon: '🔍', | |||
url: { | |||
pathname: '/', | |||
}, | |||
@@ -10,7 +10,7 @@ export const sidebarMenuItems = [ | |||
{ | |||
id: 'compose', | |||
label: 'Compose', | |||
icon: 'C', | |||
icon: '🎵', | |||
url: { | |||
pathname: '/my/create/ringtones', | |||
}, | |||
@@ -18,7 +18,7 @@ export const sidebarMenuItems = [ | |||
{ | |||
id: 'bin', | |||
label: 'Bin', | |||
icon: 'B', | |||
icon: '🗑', | |||
url: { | |||
pathname: '/my/bin', | |||
}, | |||
@@ -27,6 +27,6 @@ export const sidebarMenuItems = [ | |||
export const moreLinkMenuItem = { | |||
label: 'More', | |||
icon: 'M', | |||
icon: '···', | |||
url: {}, | |||
} |
@@ -15,14 +15,24 @@ export const get = (id: string): FetchClientParams => ({ | |||
url: ['', 'api', 'ringtones', encodeURIComponent(id)].join('/'), | |||
}) | |||
export const browse = ({ skip, take, }: { skip?: number, take?: number }): FetchClientParams => ({ | |||
method: Method.GET, | |||
url: ['', 'api', 'ringtones'].join('/'), | |||
query: { | |||
skip: typeof skip === 'number' ? skip.toString() : undefined, | |||
take: typeof take === 'number' ? take.toString() : undefined, | |||
}, | |||
}) | |||
export const browse = ({ skip, take, }: { skip?: number, take?: number }): FetchClientParams => { | |||
const params: FetchClientParams = { | |||
method: Method.GET, | |||
url: ['', 'api', 'ringtones'].join('/'), | |||
} | |||
if (Number.isFinite(skip) || Number.isFinite(take)) { | |||
params.query = {} | |||
if (Number.isFinite(skip)) { | |||
params.query.skip = skip.toString() | |||
} | |||
if (Number.isFinite(take)) { | |||
params.query.take = take.toString() | |||
} | |||
} | |||
return params | |||
} | |||
export const update = (id: string) => (body: Partial<models.Ringtone>): FetchClientParams => ({ | |||
method: Method.PATCH, | |||
@@ -2,8 +2,8 @@ import { createGlobalStyle } from 'styled-components' | |||
const GlobalStyle = createGlobalStyle({ | |||
':root': { | |||
'--color-bg': 'white', | |||
'--color-fg': 'black', | |||
'--color-bg': '#eee', | |||
'--color-fg': '#333', | |||
color: 'var(--color-fg, black)', | |||
backgroundColor: 'var(--color-bg, white)', | |||
fontFamily: 'system-ui, sans-serif', | |||
@@ -16,8 +16,8 @@ const GlobalStyle = createGlobalStyle({ | |||
}, | |||
'@media (prefers-color-scheme: dark)': { | |||
':root': { | |||
'--color-bg': 'black', | |||
'--color-fg': 'white', | |||
'--color-bg': '#222', | |||
'--color-fg': '#eee', | |||
color: 'var(--color-fg, white)', | |||
backgroundColor: 'var(--color-bg, black)', | |||
}, | |||
@@ -2,12 +2,12 @@ import {GetServerSideProps, NextPage} from 'next' | |||
import {getSession, Session, withPageAuthRequired} from '@auth0/nextjs-auth0'; | |||
import {useEffect, useState} from 'react'; | |||
import {models} from '@tonality/library-common' | |||
import {useRouter} from 'next/router'; | |||
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | |||
import RingtoneClient from '../../../../modules/ringtone/client' | |||
import WaveOscillator from '../../../../utils/sound/WaveOscillator' | |||
import SoundManager from '../../../../utils/sound/SoundManager' | |||
import ComposerClient from '../../../../modules/composer/client' | |||
import {useRouter} from 'next/router'; | |||
type Props = { | |||
session: Partial<Session>, | |||
@@ -82,7 +82,7 @@ export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||
}, | |||
} | |||
}, | |||
returnTo: '/my/create/ringtone' | |||
returnTo: '/my/create/ringtones' | |||
}) | |||
export default Page |
@@ -1,7 +1,6 @@ | |||
import {GetServerSideProps, NextPage} from 'next' | |||
import {getSession, Session, withPageAuthRequired} from '@auth0/nextjs-auth0'; | |||
import {useEffect, useState} from 'react'; | |||
import {models} from '@tonality/library-common' | |||
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | |||
import RingtoneClient from '../../../../modules/ringtone/client' | |||
import WaveOscillator from '../../../../utils/sound/WaveOscillator' | |||
@@ -69,7 +68,7 @@ export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||
}, | |||
} | |||
}, | |||
returnTo: '/my/create/ringtone' | |||
returnTo: '/my/create/ringtones' | |||
}) | |||
export default Page |