The browsing of ringtones are now in a pagination-based format. The response from the backend has now been changed in order to account the total ringtones count, although there can be some optimizations regarding the repetitive queries in the future (such as pagination tokens, caching). The ringtones lack a way to get the composer's user details such as usernames; this will be implemented in the future.master
@@ -27,8 +27,13 @@ | |||||
- As a client, I want to search for ringtones. | - As a client, I want to search for ringtones. | ||||
- [ ] In the front-end, the client requests provides a search keyword to request for ringtones from the back-end. | - [ ] In the front-end, the client requests provides a search keyword to request for ringtones from the back-end. | ||||
- [X] In the back-end, the server retrieves the ringtones whose name matches the search keyword provided by the | |||||
- [ ] In the back-end, the server retrieves the ringtones whose name matches the search keyword provided by the | |||||
front-end. | front-end. | ||||
- [X] The server must retrieve ringtones containing the search keywords in the name | |||||
- Query `foo bar` must match names containing `foo` _or_ `bar`. | |||||
- [ ] The server must retrieve ringtones with note data similar to the search keyword as melody | |||||
- The server should match data `8c4 8d#5` and `4c5 4d#5` for the query `c d#`. | |||||
- The search should ignore rests. | |||||
- As a client, I want to search for composers. | - As a client, I want to search for composers. | ||||
- [ ] In the front-end, the client requests provides a search keyword to request for composers from the back-end. | - [ ] In the front-end, the client requests provides a search keyword to request for composers from the back-end. | ||||
- [X] In the back-end, the server retrieves the composers whose name matches the search keyword provided by the | - [X] In the back-end, the server retrieves the composers whose name matches the search keyword provided by the | ||||
@@ -0,0 +1,40 @@ | |||||
import {FC} from 'react'; | |||||
import styled from 'styled-components'; | |||||
const Base = styled('div')({ | |||||
borderRadius: '0.25rem', | |||||
position: 'relative', | |||||
padding: '1rem', | |||||
boxSizing: 'border-box', | |||||
'::after': { | |||||
content: "''", | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: '100%', | |||||
boxSizing: 'border-box', | |||||
borderRadius: 'inherit', | |||||
pointerEvents: 'none', | |||||
border: '1px solid', | |||||
}, | |||||
}) | |||||
type Props = { | |||||
className?: string, | |||||
} | |||||
const Card: FC<Props> = ({ | |||||
children, | |||||
...etcProps | |||||
}) => { | |||||
return ( | |||||
<Base | |||||
{...etcProps} | |||||
> | |||||
{children} | |||||
</Base> | |||||
) | |||||
} | |||||
export default Card |
@@ -0,0 +1,68 @@ | |||||
import {FC} from 'react'; | |||||
import styled from 'styled-components'; | |||||
import {formatCreatedAt} from '../../../../utils/presentation/date'; | |||||
const Primary = styled('div')({ | |||||
display: 'grid', | |||||
gridTemplateColumns: 'repeat(2, 1fr)', | |||||
justifyContent: 'space-between', | |||||
alignItems: 'center', | |||||
gap: '0.5rem', | |||||
'@media (min-width: 720px)': { | |||||
gridTemplateColumns: '1fr', | |||||
}, | |||||
}) | |||||
const Name = styled('div')({ | |||||
fontWeight: 'bolder', | |||||
}) | |||||
const CreatedAt = styled('time')({ | |||||
fontSize: '0.75rem', | |||||
textAlign: 'right', | |||||
'@media (min-width: 720px)': { | |||||
textAlign: 'left', | |||||
}, | |||||
}) | |||||
const Data = styled('div')({ | |||||
marginTop: '0.5rem', | |||||
fontFamily: 'monospace', | |||||
fontSize: '0.75rem', | |||||
height: '3.75em', | |||||
lineHeight: 1.25, | |||||
overflow: 'hidden', | |||||
}) | |||||
type Props = { | |||||
name: string, | |||||
data: string, | |||||
createdAt: Date, | |||||
} | |||||
const RingtoneCardDisplay: FC<Props> = ({ | |||||
name, | |||||
data, | |||||
createdAt: rawCreatedAt, | |||||
}) => { | |||||
const createdAt = new Date(rawCreatedAt) | |||||
return ( | |||||
<> | |||||
<Primary> | |||||
<Name> | |||||
{name} | |||||
</Name> | |||||
<CreatedAt | |||||
dateTime={createdAt.toISOString()} | |||||
> | |||||
{formatCreatedAt(createdAt)} | |||||
</CreatedAt> | |||||
</Primary> | |||||
<Data> | |||||
{data} | |||||
</Data> | |||||
</> | |||||
) | |||||
} | |||||
export default RingtoneCardDisplay |
@@ -0,0 +1,237 @@ | |||||
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%', | |||||
}) | |||||
const CardList = styled('div')({ | |||||
display: 'grid', | |||||
gap: '1rem', | |||||
margin: '2rem 0', | |||||
'@media (min-width: 720px)': { | |||||
gridTemplateColumns: 'repeat(2, 1fr)', | |||||
}, | |||||
}) | |||||
const CardLink = styled(Link)({ | |||||
display: 'block', | |||||
textDecoration: 'none', | |||||
}) | |||||
const StyledCard = styled(Card)({ | |||||
height: '100%', | |||||
}) | |||||
const Footer = styled('div')({ | |||||
display: 'grid', | |||||
gap: '1rem', | |||||
gridTemplateColumns: '1fr', | |||||
margin: '2rem 0', | |||||
'@media (min-width: 720px)': { | |||||
gridTemplateColumns: 'auto 1fr auto', | |||||
}, | |||||
}) | |||||
const TakeForm = styled('form')({ | |||||
display: 'grid', | |||||
gap: '1rem', | |||||
gridTemplateColumns: '1fr auto', | |||||
}) | |||||
const BrowseArea = styled('div')({ | |||||
'@media (min-width: 720px)': { | |||||
gridColumn: 3, | |||||
}, | |||||
}) | |||||
const TakeInput = styled(NumericInput)({ | |||||
'@media (min-width: 720px)': { | |||||
width: '6rem', | |||||
}, | |||||
}) | |||||
type Props = { | |||||
onSearch?: FormEventHandler, | |||||
onNextPage?: FormEventHandler, | |||||
session?: Partial<Session>, | |||||
ringtones: models.Ringtone[], | |||||
skip?: number, | |||||
take?: number, | |||||
total?: number, | |||||
loading?: boolean, | |||||
} | |||||
const BrowseRingtonesTemplate: FC<Props> = ({ | |||||
onSearch, | |||||
session, | |||||
ringtones, | |||||
skip = 0, | |||||
take = 10, | |||||
total = 0, | |||||
onNextPage, | |||||
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} | |||||
> | |||||
<LeftSidebarWithMenu.ContentContainer> | |||||
{ | |||||
Array.isArray(ringtones) | |||||
&& ( | |||||
<> | |||||
<CardList> | |||||
{ringtones.map(r => ( | |||||
<CardLink | |||||
key={r.id.toString()} | |||||
href={{ | |||||
pathname: '/ringtones/[id]', | |||||
query: { | |||||
id: r.id.toString(), | |||||
}, | |||||
}} | |||||
> | |||||
<StyledCard> | |||||
<RingtoneCardDisplay | |||||
name={r.name} | |||||
data={r.data} | |||||
createdAt={r.createdAt} | |||||
/> | |||||
</StyledCard> | |||||
</CardLink> | |||||
))} | |||||
</CardList> | |||||
<Footer> | |||||
<TakeForm> | |||||
<TakeInput | |||||
label="Visible Items" | |||||
name="take" | |||||
defaultValue={take} | |||||
/> | |||||
<ActionButton | |||||
type="submit" | |||||
disabled={loading} | |||||
> | |||||
Set | |||||
</ActionButton> | |||||
</TakeForm> | |||||
<BrowseArea> | |||||
{ | |||||
skip + take < total | |||||
&& ( | |||||
<form | |||||
onSubmit={onNextPage} | |||||
> | |||||
<input | |||||
type="hidden" | |||||
name="skip" | |||||
value={skip + take} | |||||
/> | |||||
<input | |||||
type="hidden" | |||||
name="take" | |||||
value={take} | |||||
/> | |||||
<ActionButton | |||||
type="submit" | |||||
disabled={loading} | |||||
block | |||||
> | |||||
Browse More | |||||
</ActionButton> | |||||
</form> | |||||
) | |||||
} | |||||
</BrowseArea> | |||||
</Footer> | |||||
</> | |||||
) | |||||
} | |||||
</LeftSidebarWithMenu.ContentContainer> | |||||
</LeftSidebarWithMenu.Layout> | |||||
) | |||||
} | |||||
export default BrowseRingtonesTemplate |
@@ -7,6 +7,7 @@ import CreateRingtoneForm from '../../organisms/forms/CreateRingtone' | |||||
import Link from '../../molecules/navigation/Link' | import Link from '../../molecules/navigation/Link' | ||||
import OmnisearchForm from '../../organisms/forms/Omnisearch' | import OmnisearchForm from '../../organisms/forms/Omnisearch' | ||||
import Brand from '../../molecules/brand/Brand' | import Brand from '../../molecules/brand/Brand' | ||||
import {moreLinkMenuItem, sidebarMenuItems} from '../../../data/layout'; | |||||
const TopBarComponent = styled('div')({ | const TopBarComponent = styled('div')({ | ||||
backgroundColor: 'var(--color-bg, white)', | backgroundColor: 'var(--color-bg, white)', | ||||
@@ -17,7 +18,7 @@ const SidebarMenuComponent = styled('div')({ | |||||
}) | }) | ||||
const Padding = styled('div')({ | const Padding = styled('div')({ | ||||
padding: '2rem 0', | |||||
margin: '2rem 0', | |||||
}) | }) | ||||
const Avatar = styled('img')({ | const Avatar = styled('img')({ | ||||
@@ -33,8 +34,6 @@ type Props = { | |||||
onSubmit?: FormEventHandler, | onSubmit?: FormEventHandler, | ||||
session: Partial<Session>, | session: Partial<Session>, | ||||
currentRingtone?: models.Ringtone, | currentRingtone?: models.Ringtone, | ||||
composerRingtones: models.Ringtone[], | |||||
updateTempo: ({ songRef, dataRef, setTempo, }) => (...args: unknown[]) => void, | updateTempo: ({ songRef, dataRef, setTempo, }) => (...args: unknown[]) => void, | ||||
updateView: ({ setNoteGlyph, setRestGlyph, noteGlyphs, restGlyphs, }) => (...args: unknown[]) => void, | updateView: ({ setNoteGlyph, setRestGlyph, noteGlyphs, restGlyphs, }) => (...args: unknown[]) => void, | ||||
addRest: ({ formRef, dataRef, songRef, }) => (...args: unknown[]) => void, | addRest: ({ formRef, dataRef, songRef, }) => (...args: unknown[]) => void, | ||||
@@ -49,7 +48,6 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||||
onSubmit, | onSubmit, | ||||
session, | session, | ||||
currentRingtone = {}, | currentRingtone = {}, | ||||
composerRingtones = [], | |||||
updateTempo, | updateTempo, | ||||
updateView, | updateView, | ||||
addRest, | addRest, | ||||
@@ -100,11 +98,7 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||||
</LeftSidebarWithMenu.SidebarMenuContainer> | </LeftSidebarWithMenu.SidebarMenuContainer> | ||||
</Link> | </Link> | ||||
)} | )} | ||||
moreLinkMenuItem={{ | |||||
label: 'More', | |||||
icon: 'M', | |||||
url: {}, | |||||
}} | |||||
moreLinkMenuItem={moreLinkMenuItem} | |||||
moreLinkComponent={({ url, icon, label, }) => ( | moreLinkComponent={({ url, icon, label, }) => ( | ||||
<Link | <Link | ||||
href={url} | href={url} | ||||
@@ -117,24 +111,7 @@ const CreateRingtoneTemplate: FC<Props> = ({ | |||||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | </LeftSidebarWithMenu.MoreSidebarMenuContainer> | ||||
</Link> | </Link> | ||||
)} | )} | ||||
sidebarMenuItems={[ | |||||
{ | |||||
id: 'browse', | |||||
label: 'Browse', | |||||
icon: 'B', | |||||
url: { | |||||
pathname: '/ringtones', | |||||
}, | |||||
}, | |||||
{ | |||||
id: 'compose', | |||||
label: 'Compose', | |||||
icon: 'C', | |||||
url: { | |||||
pathname: '/my/create/ringtone', | |||||
}, | |||||
}, | |||||
]} | |||||
sidebarMenuItems={sidebarMenuItems} | |||||
> | > | ||||
<Padding> | <Padding> | ||||
<CreateRingtoneForm | <CreateRingtoneForm | ||||
@@ -0,0 +1,32 @@ | |||||
export const sidebarMenuItems = [ | |||||
{ | |||||
id: 'browse', | |||||
label: 'Browse', | |||||
icon: 'B', | |||||
url: { | |||||
pathname: '/', | |||||
}, | |||||
}, | |||||
{ | |||||
id: 'compose', | |||||
label: 'Compose', | |||||
icon: 'C', | |||||
url: { | |||||
pathname: '/my/create/ringtones', | |||||
}, | |||||
}, | |||||
{ | |||||
id: 'bin', | |||||
label: 'Bin', | |||||
icon: 'B', | |||||
url: { | |||||
pathname: '/my/bin', | |||||
}, | |||||
}, | |||||
] | |||||
export const moreLinkMenuItem = { | |||||
label: 'More', | |||||
icon: 'M', | |||||
url: {}, | |||||
} |
@@ -52,4 +52,11 @@ export default class RingtoneClient { | |||||
return data | return data | ||||
} | } | ||||
} | } | ||||
browse = async ({ skip, take }) => { | |||||
const response = await this.fetchClient(endpoints.browse({ skip, take, })) | |||||
if (response.ok) { | |||||
return response.json() | |||||
} | |||||
} | |||||
} | } |
@@ -1,17 +1,100 @@ | |||||
import {GetServerSideProps, NextPage} from 'next'; | import {GetServerSideProps, NextPage} from 'next'; | ||||
type Props = {} | |||||
import BrowseRingtonesTemplate from '../components/templates/BrowseRingtones'; | |||||
import {getSession, Session} from '@auth0/nextjs-auth0'; | |||||
import {models} from '@tonality/library-common'; | |||||
import RingtoneClient from '../modules/ringtone/client'; | |||||
import {useEffect, useState} from 'react'; | |||||
import getFormValues from '@theoryofnekomata/formxtra'; | |||||
import {useRouter} from 'next/router'; | |||||
type Props = { | |||||
session: Partial<Session>, | |||||
ringtones: models.Ringtone[], | |||||
skip: number, | |||||
take: number, | |||||
total: number, | |||||
} | |||||
const IndexPage: NextPage<Props> = ({ | |||||
session, | |||||
ringtones: ringtonesProp, | |||||
skip: skipProp, | |||||
take: takeProp, | |||||
total: totalProp, | |||||
}) => { | |||||
const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | |||||
const [ringtones, setRingtones] = useState(ringtonesProp) | |||||
const [skip, setSkip] = useState(skipProp) | |||||
const [take, setTake] = useState(takeProp) | |||||
const [total, setTotal] = useState(totalProp) | |||||
const [loading, setLoading] = useState(false) | |||||
const router = useRouter() | |||||
const getNextPage = async (e) => { | |||||
// e.preventDefault() | |||||
// const values = getFormValues(e.target) | |||||
// setLoading(true) | |||||
// try { | |||||
// const {data: ringtones, skip, take, total} = await ringtoneClient.browse({ | |||||
// skip: Number(values.skip), | |||||
// take: Number(values.take) | |||||
// }) | |||||
// setRingtones(ringtones) | |||||
// setSkip(skip) | |||||
// setTake(take) | |||||
// setTotal(total) | |||||
// router.push({ | |||||
// query: { | |||||
// skip: Number(values.skip), | |||||
// take: Number(values.take), | |||||
// }, | |||||
// }) | |||||
// } catch (err) { | |||||
// console.log(err) | |||||
// } | |||||
// setLoading(false) | |||||
} | |||||
useEffect(() => { | |||||
setRingtoneClient(new RingtoneClient(process.env.NEXT_PUBLIC_API_BASE_URL, session)) | |||||
}, []) | |||||
const IndexPage: NextPage<Props> = () => { | |||||
return ( | return ( | ||||
<> | |||||
Landing Page Here | |||||
</> | |||||
<BrowseRingtonesTemplate | |||||
session={session} | |||||
ringtones={ringtones} | |||||
skip={skip} | |||||
take={take} | |||||
total={total} | |||||
loading={loading} | |||||
onNextPage={getNextPage} | |||||
/> | |||||
); | ); | ||||
}; | }; | ||||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||||
export const getServerSideProps: GetServerSideProps = async ({ req, res, query }) => { | |||||
const authSession = getSession(req, res) | |||||
const session = authSession | |||||
? { | |||||
idToken: authSession.idToken, | |||||
token_type: authSession.token_type, | |||||
user: authSession.user, | |||||
} | |||||
: null | |||||
const client = new RingtoneClient(process.env.NEXT_PUBLIC_API_BASE_URL, session) | |||||
const { data: ringtones, skip, take, total } = await client.browse({ | |||||
skip: query.skip ? Number(query.skip) : undefined, | |||||
take: query.take ? Number(query.take) : undefined, | |||||
}) | |||||
return { | return { | ||||
props: {} | |||||
props: { | |||||
session, | |||||
ringtones, | |||||
skip, | |||||
take, | |||||
total, | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -12,13 +12,11 @@ import {useRouter} from 'next/router'; | |||||
type Props = { | type Props = { | ||||
session: Partial<Session>, | session: Partial<Session>, | ||||
currentRingtone: models.Ringtone, | currentRingtone: models.Ringtone, | ||||
composerRingtones: models.Ringtone[], | |||||
} | } | ||||
const Page: NextPage<Props> = ({ | const Page: NextPage<Props> = ({ | ||||
session, | session, | ||||
currentRingtone, | currentRingtone, | ||||
composerRingtones, | |||||
}) => { | }) => { | ||||
const [hydrated, setHydrated] = useState(false) | const [hydrated, setHydrated] = useState(false) | ||||
const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | ||||
@@ -47,7 +45,6 @@ const Page: NextPage<Props> = ({ | |||||
<CreateRingtoneTemplate | <CreateRingtoneTemplate | ||||
session={session} | session={session} | ||||
currentRingtone={currentRingtone} | currentRingtone={currentRingtone} | ||||
composerRingtones={composerRingtones} | |||||
addNote={composerClient ? composerClient.addNote : undefined} | addNote={composerClient ? composerClient.addNote : undefined} | ||||
addRest={composerClient ? composerClient.addRest : undefined} | addRest={composerClient ? composerClient.addRest : undefined} | ||||
togglePlayback={composerClient ? composerClient.togglePlayback : undefined} | togglePlayback={composerClient ? composerClient.togglePlayback : undefined} | ||||
@@ -64,7 +61,6 @@ export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||||
getServerSideProps: async ({ req, res, params }) => { | getServerSideProps: async ({ req, res, params }) => { | ||||
const { id } = params | const { id } = params | ||||
const { idToken, token_type, user } = getSession(req, res) | const { idToken, token_type, user } = getSession(req, res) | ||||
const composerRingtones = [] | |||||
const session = { | const session = { | ||||
idToken, | idToken, | ||||
token_type, | token_type, | ||||
@@ -83,7 +79,6 @@ export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||||
props: { | props: { | ||||
session, | session, | ||||
currentRingtone, | currentRingtone, | ||||
composerRingtones, | |||||
}, | }, | ||||
} | } | ||||
}, | }, | ||||
@@ -11,12 +11,10 @@ import {useRouter} from 'next/router'; | |||||
type Props = { | type Props = { | ||||
session: Partial<Session>, | session: Partial<Session>, | ||||
composerRingtones: models.Ringtone[], | |||||
} | } | ||||
const Page: NextPage<Props> = ({ | const Page: NextPage<Props> = ({ | ||||
session, | session, | ||||
composerRingtones, | |||||
}) => { | }) => { | ||||
const [hydrated, setHydrated] = useState(false) | const [hydrated, setHydrated] = useState(false) | ||||
const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | const [ringtoneClient, setRingtoneClient] = useState<RingtoneClient>(null) | ||||
@@ -44,7 +42,6 @@ const Page: NextPage<Props> = ({ | |||||
return ( | return ( | ||||
<CreateRingtoneTemplate | <CreateRingtoneTemplate | ||||
session={session} | session={session} | ||||
composerRingtones={composerRingtones} | |||||
addNote={composerClient ? composerClient.addNote : undefined} | addNote={composerClient ? composerClient.addNote : undefined} | ||||
addRest={composerClient ? composerClient.addRest : undefined} | addRest={composerClient ? composerClient.addRest : undefined} | ||||
togglePlayback={composerClient ? composerClient.togglePlayback : undefined} | togglePlayback={composerClient ? composerClient.togglePlayback : undefined} | ||||
@@ -60,7 +57,6 @@ const Page: NextPage<Props> = ({ | |||||
export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | ||||
getServerSideProps: async (ctx) => { | getServerSideProps: async (ctx) => { | ||||
const { idToken, token_type, user } = getSession(ctx.req, ctx.res) | const { idToken, token_type, user } = getSession(ctx.req, ctx.res) | ||||
const composerRingtones = [] | |||||
const session = { | const session = { | ||||
idToken, | idToken, | ||||
token_type, | token_type, | ||||
@@ -70,7 +66,6 @@ export const getServerSideProps: GetServerSideProps = withPageAuthRequired({ | |||||
return { | return { | ||||
props: { | props: { | ||||
session, | session, | ||||
composerRingtones, | |||||
}, | }, | ||||
} | } | ||||
}, | }, | ||||
@@ -1,5 +1,3 @@ | |||||
import {URLSearchParams} from 'url'; | |||||
export enum Method { | export enum Method { | ||||
HEAD = 'HEAD', | HEAD = 'HEAD', | ||||
GET = 'GET', | GET = 'GET', | ||||
@@ -45,7 +43,7 @@ export const createFetchClient: CreateFetchClient = ({ | |||||
}) => { | }) => { | ||||
const theUrl = new URL(url, baseUrl) | const theUrl = new URL(url, baseUrl) | ||||
if (Boolean(query as unknown)) { | if (Boolean(query as unknown)) { | ||||
theUrl.search = new URLSearchParams(query).toString() | |||||
theUrl.search = new URLSearchParams(query as any).toString() | |||||
} | } | ||||
return ff(theUrl.toString(), { | return ff(theUrl.toString(), { | ||||
method, | method, | ||||
@@ -0,0 +1,7 @@ | |||||
export const formatCreatedAt = (d: Date) => { | |||||
const month = d.getMonth().toString().padStart(2, '0') | |||||
const date = d.getDate().toString().padStart(2, '0') | |||||
const year = d.getFullYear().toString().padStart(4, '0') | |||||
return `${year}-${month}-${date}` | |||||
} |
@@ -70,10 +70,11 @@ export class RingtoneControllerImpl implements RingtoneController { | |||||
const skip = !isNaN(skipNumber) ? skipNumber : 0 | const skip = !isNaN(skipNumber) ? skipNumber : 0 | ||||
const take = !isNaN(takeNumber) ? takeNumber : 10 | const take = !isNaN(takeNumber) ? takeNumber : 10 | ||||
const rawData = await this.ringtoneService.browse(skip, take, request.user?.sub) | const rawData = await this.ringtoneService.browse(skip, take, request.user?.sub) | ||||
const data = rawData.map(serializeRingtone) | |||||
const data = rawData.data.map(serializeRingtone) | |||||
reply.raw.statusMessage = 'Multiple Ringtones Retrieved' | reply.raw.statusMessage = 'Multiple Ringtones Retrieved' | ||||
return { | return { | ||||
total: rawData.total, | |||||
data, | data, | ||||
skip, | skip, | ||||
take, | take, | ||||
@@ -88,10 +89,11 @@ export class RingtoneControllerImpl implements RingtoneController { | |||||
try { | try { | ||||
const { 'q': query } = request.query | const { 'q': query } = request.query | ||||
const rawData = await this.ringtoneService.search(query, request.user?.sub) | const rawData = await this.ringtoneService.search(query, request.user?.sub) | ||||
const data = rawData.map(serializeRingtone) | |||||
const data = rawData.data.map(serializeRingtone) | |||||
reply.raw.statusMessage = 'Search Results Retrieved' | reply.raw.statusMessage = 'Search Results Retrieved' | ||||
return { | return { | ||||
total: rawData.total, | |||||
data, | data, | ||||
} | } | ||||
} catch (err) { | } catch (err) { | ||||
@@ -9,11 +9,16 @@ import { | |||||
UpdateDeletedRingtoneError, | UpdateDeletedRingtoneError, | ||||
} from './response'; | } from './response'; | ||||
type Counted<T> = { | |||||
total: number, | |||||
data: T[], | |||||
} | |||||
export default interface RingtoneService { | export default interface RingtoneService { | ||||
get(id: Uuid, requesterUserSub?: string): Promise<models.Ringtone> | get(id: Uuid, requesterUserSub?: string): Promise<models.Ringtone> | ||||
browseByComposer(composerUserSub: string, skip?: number, take?: number, requesterUserSub?: string): Promise<models.Ringtone[]> | |||||
browse(skip?: number, take?: number, requesterUserSub?: string): Promise<models.Ringtone[]> | |||||
search(q?: string, requesterUserSub?: string): Promise<models.Ringtone[]> | |||||
browseByComposer(composerUserSub: string, skip?: number, take?: number, requesterUserSub?: string): Promise<Counted<models.Ringtone>> | |||||
browse(skip?: number, take?: number, requesterUserSub?: string): Promise<Counted<models.Ringtone>> | |||||
search(q?: string, requesterUserSub?: string): Promise<Counted<models.Ringtone>> | |||||
create(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> | create(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> | ||||
update(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> | update(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> | ||||
softDelete(id: Uuid, requesterUserSub: string): Promise<models.Ringtone> | softDelete(id: Uuid, requesterUserSub: string): Promise<models.Ringtone> | ||||
@@ -35,35 +40,30 @@ export class RingtoneServiceImpl implements RingtoneService { | |||||
private readonly prismaClient: PrismaClient, | private readonly prismaClient: PrismaClient, | ||||
) {} | ) {} | ||||
async browseByComposer(composerUserSub: string, skip: number = 0, take: number = 10, requesterUserSub?: string): Promise<models.Ringtone[]> { | |||||
let rawData | |||||
if (typeof requesterUserSub === 'string' && composerUserSub === requesterUserSub) { | |||||
rawData = await this.prismaClient.ringtone.findMany({ | |||||
where: { | |||||
composerUserSub: { | |||||
equals: composerUserSub, | |||||
}, | |||||
}, | |||||
skip, | |||||
take, | |||||
}) | |||||
} else { | |||||
rawData = await this.prismaClient.ringtone.findMany({ | |||||
where: { | |||||
composerUserSub: { | |||||
equals: composerUserSub, | |||||
}, | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | |||||
async browseByComposer(composerUserSub: string, skip: number = 0, take: number = 10, requesterUserSub?: string): Promise<Counted<models.Ringtone>> { | |||||
const baseArgs = { | |||||
where: { | |||||
composerUserSub: { | |||||
equals: composerUserSub, | |||||
}, | }, | ||||
skip, | |||||
take, | |||||
}) | |||||
}, | |||||
} | |||||
if (!(typeof requesterUserSub === 'string' && composerUserSub === requesterUserSub)) { | |||||
baseArgs.where['deletedAt'] = { | |||||
equals: null, | |||||
} | |||||
} | |||||
const args = { | |||||
...baseArgs, | |||||
skip, | |||||
take, | |||||
} | |||||
const rawData = await this.prismaClient.ringtone.findMany(baseArgs) | |||||
const total = await this.prismaClient.ringtone.count(args) | |||||
return { | |||||
data: rawData.map(serializeRingtone), | |||||
total, | |||||
} | } | ||||
return rawData.map(serializeRingtone) | |||||
} | } | ||||
async get(id: Uuid, requesterUserSub?: string): Promise<models.Ringtone> { | async get(id: Uuid, requesterUserSub?: string): Promise<models.Ringtone> { | ||||
@@ -89,77 +89,72 @@ export class RingtoneServiceImpl implements RingtoneService { | |||||
return serializeRingtone(ringtone) | return serializeRingtone(ringtone) | ||||
} | } | ||||
async browse(skip: number = 0, take: number = 10, requesterUserSub?: string): Promise<models.Ringtone[]> { | |||||
let rawData | |||||
if (typeof requesterUserSub === 'string') { | |||||
rawData = await this.prismaClient.ringtone.findMany({ | |||||
where: { | |||||
OR: [ | |||||
{ | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | |||||
async browse(skip: number = 0, take: number = 10, requesterUserSub?: string): Promise<Counted<models.Ringtone>> { | |||||
const baseArgs = { | |||||
where: { | |||||
OR: [ | |||||
{ | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | }, | ||||
{ | |||||
composerUserSub: { | |||||
equals: requesterUserSub, | |||||
}, | |||||
} | |||||
], | |||||
}, | |||||
skip, | |||||
take, | |||||
}) | |||||
} else { | |||||
rawData = await this.prismaClient.ringtone.findMany({ | |||||
where: { | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | |||||
} as Record<string, unknown>,, | |||||
] | |||||
}, | |||||
} | |||||
if (typeof requesterUserSub === 'string') { | |||||
baseArgs.where.OR.push({ | |||||
composerUserSub: { | |||||
equals: requesterUserSub, | |||||
}, | }, | ||||
skip, | |||||
take, | |||||
}) | }) | ||||
} | } | ||||
return rawData.map(serializeRingtone) | |||||
const args = { | |||||
...baseArgs, | |||||
skip, | |||||
take, | |||||
} | |||||
const rawData = await this.prismaClient.ringtone.findMany(args) | |||||
const total = await this.prismaClient.ringtone.count(baseArgs) | |||||
return { | |||||
total, | |||||
data: rawData.map(serializeRingtone) | |||||
} | |||||
} | } | ||||
async search(q: string = '', requesterUserSub?: string): Promise<models.Ringtone[]> { | |||||
let rawData | |||||
if (typeof requesterUserSub === 'string') { | |||||
rawData = this.prismaClient.ringtone.findMany({ | |||||
where: { | |||||
OR: [ | |||||
{ | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | |||||
}, | |||||
{ | |||||
composerUserSub: { | |||||
equals: requesterUserSub, | |||||
}, | |||||
async search(q: string = '', requesterUserSub?: string): Promise<Counted<models.Ringtone>> { | |||||
const baseArgs = { | |||||
where: { | |||||
OR: [ | |||||
{ | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | }, | ||||
], | |||||
name: { | |||||
contains: q, | |||||
}, | |||||
} as Record<string, unknown>, | |||||
], | |||||
name: { | |||||
contains: q, | |||||
}, | }, | ||||
}) | |||||
} else { | |||||
rawData = this.prismaClient.ringtone.findMany({ | |||||
where: { | |||||
name: { | |||||
contains: q, | |||||
}, | |||||
deletedAt: { | |||||
equals: null, | |||||
}, | |||||
}, | |||||
} | |||||
if (typeof requesterUserSub === 'string') { | |||||
baseArgs.where.OR.push({ | |||||
composerUserSub: { | |||||
equals: requesterUserSub, | |||||
}, | }, | ||||
}) | |||||
},) | |||||
} | |||||
const rawData = await this.prismaClient.ringtone.findMany(baseArgs) | |||||
const total = await this.prismaClient.ringtone.count(baseArgs) | |||||
return { | |||||
data: rawData.map(serializeRingtone), | |||||
total, | |||||
} | } | ||||
return rawData.map(serializeRingtone) | |||||
} | } | ||||
async create(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> { | async create(data: Partial<models.Ringtone>, requesterUserSub: string): Promise<models.Ringtone> { | ||||