Skip to content

Commit

Permalink
wip(artist): add artist edit page
Browse files Browse the repository at this point in the history
  • Loading branch information
nikolvs committed Jul 27, 2024
1 parent 9fbfb46 commit 95af13a
Show file tree
Hide file tree
Showing 23 changed files with 422 additions and 65 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ DATABASE_URL="file:./local.db"
# App variables
NEXT_PUBLIC_APP_TITLE=Geração 666 | Diretoria
NEXT_PUBLIC_APP_DESCRIPTION=Gerenciador de discografias do Geração 666.
NEXT_PUBLIC_APP_IMAGE_BASE_URL=https://geracao666.com.br/images/

# GitHub Auth
GITHUB_API_TOKEN=
Expand Down
3 changes: 3 additions & 0 deletions app/api/artist/artist.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ export const getArtistsPaginated = async ({ page = 1, limit = 15 }: PaginationPa
take: limit,
include: {
tags: true
},
orderBy: {
updatedAt: 'desc'
}
})

Expand Down
3 changes: 3 additions & 0 deletions app/artist/ArtistForm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ArtistFormProps = {
slug?: string
}
74 changes: 74 additions & 0 deletions app/artist/ArtistForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client'

import useAxios from "@/app/lib/axios";
import { ArtistFormProps } from "./ArtistForm.d";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { Artist, artistSchema } from "./artist.schema";
import { resolver } from '@/app/lib/yup'
import { Form, FormAutocomplete, FormButton, FormImageInput, FormInput } from "../components/form";
import { APP_IMAGE_BASE_URL } from "../config";

export default function ArtistForm({
slug
}: ArtistFormProps) {
const [{ data: tags = [], loading: loadingTags }] = useAxios<string[]>('/api/tag/index.json')
const [{ data: artist }, fetchArtist] = useAxios({
url: `/api/artist/${slug}`,
method: 'GET'
}, {
manual: true
})

const context = useForm<Artist>({
...resolver(artistSchema)
})

useEffect(() => {
if (slug) {
fetchArtist()
}
}, [slug, fetchArtist])

useEffect(() => {
if (artist) {
context.reset({
...artist,
cover: `${APP_IMAGE_BASE_URL}${artist.cover}`
})
}
}, [artist, context])

console.log('form', context.getValues())

return (
<Form context={context}>
<div className="grid grid-cols-1 max-w-sm">
<div>
<FormInput label="Nome do artista/banda" name="name" />
<FormInput label="País de origem" name="origin" />
<FormImageInput label="Imagem da capa" name="cover" />

<FormAutocomplete
name="tags"
label="Tags"
items={
tags.map((tag: string) => ({
label: tag,
value: tag
}))
}
disabled={loadingTags}
loading={loadingTags}
multiple
/>
</div>

<div className="flex flex-row gap-1">
<FormButton variant="outline" className="w-1/2">Cancelar</FormButton>
<FormButton type="submit" className="w-1/2" disabled>Salvar</FormButton>
</div>
</div>
</Form>
)
}
13 changes: 13 additions & 0 deletions app/artist/[slug]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PageHeader } from "@/app/components/page";
import ArtistForm from "../../ArtistForm";

export default function ArtistEditPage({ params }: {
params: { slug: string }
}) {
return (
<div>
<PageHeader title="Editar artista" />
<ArtistForm slug={params.slug} />
</div>
)
}
2 changes: 1 addition & 1 deletion app/artist/artist.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const artistSchema = yup.object({
allowedFormats: ['jpg', 'png']
}).label('Imagem da capa').required(),
origin: yup.string().label('País de origem').required(),
genres: yup.array(yup.string()).label('Gêneros musicais').optional(),
tags: yup.array(yup.string()).label('Tags').optional(),
})

export type Artist = yup.InferType<typeof artistSchema>
3 changes: 1 addition & 2 deletions app/artist/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useForm } from "react-hook-form";
import { Artist, artistSchema } from "../artist.schema";
import { resolver } from '@/app/lib/yup'
import useAxios from "@/app/lib/axios";
import { Tag } from "@prisma/client";
import ReactCrop, { Crop, PercentCrop } from 'react-image-crop'
import "react-image-crop/dist/ReactCrop.css";
import { useRef, useState } from "react";
Expand Down Expand Up @@ -124,7 +123,7 @@ export default function ArtistCreatePage() {
{coverDataUrl && !errors.cover && <ImagePreview
className="mb-4"
imageRef={imgRef}
crop={percentCrop}
percentCrop={percentCrop}
exportWidth={cropWidth}
exportHeight={cropHeight}
onChange={setCropDataUrl}
Expand Down
77 changes: 55 additions & 22 deletions app/artist/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
'use client'

import { Badge, Divider, Table } from "react-daisyui";
import { Badge, Button, Checkbox, Divider, Loading, Table } from "react-daisyui";
import { PageHeader } from "../components/page";
import useAxios from "axios-hooks";
import { Pagination } from "../components/pagination";
import { PaginationProps } from "../components/pagination/Pagination.d";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import { MdEdit } from "react-icons/md";
import { useRouter } from "next/navigation";

export default function ArtistPage() {
const router = useRouter()
const [pagination, setPagination] = useState<PaginationProps>({
page: 1,
limit: 15
limit: 10
})

const [{ data: artists = [], loading }] = useAxios({
Expand All @@ -32,47 +36,76 @@ export default function ArtistPage() {
setPagination((pagination) => ({ ...pagination, page }))
}

const editArtist = (slug: string) => () => {
router.push(`/artist/${slug}/edit`)
}

return (
<div>
<PageHeader title="Artistas" />

<Table pinRows>
<Table size="sm" pinRows>
<thead>
<tr className="bg-zinc-950 text-neutral-100">
<th><span>Publicado?</span></th>
<th><span>Nome</span></th>
<th><span>Origem</span></th>
<th><span>Tags</span></th>
<th><span>Última atualização</span></th>
<th><span></span></th>
</tr>
</thead>

<Table.Body>
{artists.data?.map(artist => (
<Table.Row key={artist.id} className="hover:bg-gray-200 transition ease-out">
<span>{artist.name}</span>
<span>{artist.origin}</span>
<span className="flex flex-wrap max-w-64 gap-1">
{artist.tags.map(tag => (
<Badge
key={tag.id}
color="neutral"
className="inline-flex"
<tr key={artist.id} className="hover:bg-gray-200 transition ease-out">
<td><span><Checkbox color="success" checked /></span></td>
<td><span className="font-bold">{artist.name}</span></td>
<td><span>{artist.origin}</span></td>
<td className="w-96">
<span className="flex flex-wrap gap-1">
{artist.tags.map(tag => (
<Badge
key={tag.id}
color="neutral"
className="inline-flex"
>
{tag.name}
</Badge>
))}
</span>
</td>
<td><span>{dayjs(artist.updatedAt).format('DD/MM/YYYY HH:mm')}</span></td>
<td>
<span>
<Button
color="ghost"
className="text-2xl p-1"
onClick={editArtist(artist.slug)}
>
{tag.name}
</Badge>
))}
</span>
<span>{artist.updatedAt}</span>
</Table.Row>
<MdEdit />
</Button>
</span>
</td>
</tr>
))}
</Table.Body>
</Table>

<Divider />
{loading && (
<div className="flex justify-center w-full p-8">
<Loading size="lg" />
</div>
)}

<div className="flex w-full justify-center">
<Pagination {...pagination} onPageSelected={handlePageSelected} />
</div>
{!loading && (
<>
<Divider />
<div className="flex w-full justify-center">
<Pagination {...pagination} onPageSelected={handlePageSelected} />
</div>
</>
)}
</div>
)
}
1 change: 1 addition & 0 deletions app/components/autocomplete/Autocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type AutocompleteReducerAction =
| { type: 'open_menu' }
| { type: 'close_menu' }
| { type: 'set_filter', text: string }
| { type: 'set_selected_items', value: AutocompleteValue, items: AutocompleteItem[] }
| { type: 'select_single_item', index: number }
| { type: 'toggle_item_selection', index: number }
| { type: 'remove_last_item' }
Expand Down
16 changes: 16 additions & 0 deletions app/components/autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ const reducer = (state: AutocompleteReducerState, action: AutocompleteReducerAct
case 'set_filter': {
return { ...state, filterText: action.text }
}
case 'set_selected_items': {
return {
...state,
dirty: false,
selectedItemsIndex: !action.value ? [] : (
Array.isArray(action.value)
? action.value.map(itemValue => action.items.findIndex(item => item.value === itemValue))
: [action.items.findIndex(item => item.value === action.value)]
)
}
}
case 'select_single_item': {
return {
...state,
Expand Down Expand Up @@ -78,6 +89,11 @@ const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(({
[items, selectedItemsIndex]
)

useEffect(() => {
// TODO: Try to do this without useEffect.
dispatch({ type: 'set_selected_items', value, items })
}, [value, items])

const containerDivRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement | null>(null)

Expand Down
4 changes: 3 additions & 1 deletion app/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import React from "react"
export default function Form<T extends FieldValues>({
children,
context,
className,
onSubmit = () => {}
}: {
children: React.ReactNode,
context: UseFormReturn<T>,
className?: string,
onSubmit?: SubmitHandler<T>
}) {
const internalOnSubmit = (data: T, event?: React.BaseSyntheticEvent) => {
Expand All @@ -21,7 +23,7 @@ export default function Form<T extends FieldValues>({
return (
<FormProvider {...context}>
<FormComponent
className="max-w-xs"
className={className}
onSubmit={context.handleSubmit(internalOnSubmit)}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion app/components/form/FormAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function FormAutocomplete({
items: AutocompleteItem[],
loading?: boolean,
disabled?: boolean,
multiple?: boolean,
multiple?: boolean
}) {
const {
control,
Expand Down
23 changes: 18 additions & 5 deletions app/components/form/FormButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ import React from "react";
import { Button } from "react-daisyui";

export default function FormButton({
children
children,
className,
variant,
type = 'button',
disabled = false
}: {
type?: 'submit' | 'button'
variant?: 'outline' | undefined
disabled?: boolean
className?: string
children: React.ReactNode
}) {
const variantClassNames = variant === 'outline'
? 'text-zinc-950 hover:bg-red-900 hover:text-neutral-100 hover:border-red-900'
: 'bg-zinc-950 text-neutral-100 hover:bg-emerald-700 hover:border-emerald-700'

return (
<Button
type="submit"
type={type}
disabled={disabled}
className={classNames(
'rounded-none uppercase mt-3',
'bg-zinc-950 text-neutral-100',
'hover:bg-red-900'
'border-zinc-950 rounded-none uppercase mt-3',
variantClassNames,
className
)}
>
{children}
Expand Down
Loading

0 comments on commit 95af13a

Please sign in to comment.