diff --git a/src/app/area/[...slug]/SingleEntryForm.tsx b/src/app/area/[...slug]/SingleEntryForm.tsx deleted file mode 100644 index 024dcbb0f..000000000 --- a/src/app/area/[...slug]/SingleEntryForm.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client' -import { ReactNode } from 'react' -import { FieldValues, FormProvider, useForm, DefaultValues, ValidationMode } from 'react-hook-form' - -export interface SingleEntryFormProps { - children: ReactNode - initialValues: DefaultValues - validationMode?: keyof ValidationMode - submitHandler: (formData: T) => Promise | void -} - -export function SingleEntryForm ({ children, initialValues, submitHandler, validationMode = 'onBlur' }: SingleEntryFormProps): ReactNode { - const form = useForm({ - mode: validationMode, - defaultValues: { ...initialValues } - }) - - const { handleSubmit, reset } = form - - return ( - - {/* eslint-disable-next-line */} -
{ - await submitHandler(data) - reset({ ...data }, { keepValues: true }) // clear isDirty flag - } - )} - > - {children} -
-
- ) -} diff --git a/src/app/editArea/[slug]/SidebarNav.tsx b/src/app/editArea/[slug]/SidebarNav.tsx index f9811308d..5ed49e55b 100644 --- a/src/app/editArea/[slug]/SidebarNav.tsx +++ b/src/app/editArea/[slug]/SidebarNav.tsx @@ -1,26 +1,42 @@ +'use client' import Link from 'next/link' -import { MapPinLine, TreeStructure, Article } from '@phosphor-icons/react/dist/ssr' +import { usePathname } from 'next/navigation' +import { MapPinLine, Graph, Article, FolderSimplePlus } from '@phosphor-icons/react/dist/ssr' +/** + * Sidebar navigation for area edit + */ export const SidebarNav: React.FC<{ slug: string }> = ({ slug }) => { + const activePath = usePathname() + /** + * Disable menu item's hover/click when own page is showing + */ + const classForActivePage = (myPath: string): string => activePath.endsWith(myPath) ? 'bg-base-300/60 pointer-events-none' : '' return ( ) } diff --git a/src/app/editArea/[slug]/components/SingleEntryForm.tsx b/src/app/editArea/[slug]/components/SingleEntryForm.tsx new file mode 100644 index 000000000..a4c1148bf --- /dev/null +++ b/src/app/editArea/[slug]/components/SingleEntryForm.tsx @@ -0,0 +1,75 @@ +'use client' +import { ReactNode } from 'react' +import { FieldValues, FormProvider, useForm, DefaultValues, ValidationMode } from 'react-hook-form' +import { SpinnerGap } from '@phosphor-icons/react/dist/ssr' +import clx from 'classnames' + +export interface SingleEntryFormProps { + children: ReactNode + initialValues: DefaultValues + validationMode?: keyof ValidationMode + submitHandler: (formData: T) => Promise | void + title: string + helperText?: string + keepValuesAfterReset?: boolean + className?: string +} + +export function SingleEntryForm ({ + children, + initialValues, + submitHandler, + validationMode = 'onBlur', + helperText, + title, + keepValuesAfterReset = true, + className = '' +}: SingleEntryFormProps): ReactNode { + const form = useForm({ + mode: validationMode, + defaultValues: { ...initialValues } + }) + + const { handleSubmit, reset, formState: { isValid, isSubmitting, isDirty } } = form + + return ( + + {/* eslint-disable-next-line */} +
{ + await submitHandler(data) + if (keepValuesAfterReset) { + reset({ ...data }) + } else { + reset() + } + } + )} + > +
+
+

{title}

+
+ {children} +
+
+
+ {helperText} + +
+
+
+
+ ) +} + +export const SubmitButton: React.FC<{ isValid: boolean, isSubmitting: boolean, isDirty: boolean }> = ({ + isValid, isSubmitting, isDirty +}) => ( + +) diff --git a/src/app/editArea/[slug]/general/AreaDescriptionForm.tsx b/src/app/editArea/[slug]/general/AreaDescriptionForm.tsx index 0981e9542..9fd099208 100644 --- a/src/app/editArea/[slug]/general/AreaDescriptionForm.tsx +++ b/src/app/editArea/[slug]/general/AreaDescriptionForm.tsx @@ -1,7 +1,7 @@ 'use client' import { useSession } from 'next-auth/react' -import { SingleEntryForm } from 'app/area/[...slug]/SingleEntryForm' +import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm' import { AREA_DESCRIPTION_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm' import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd' import { MDTextArea } from '@/components/ui/form/MDTextArea' @@ -22,6 +22,8 @@ export const AreaDescriptionForm: React.FC<{ initialValue: string, uuid: string return ( + title='Description' + helperText='You can use markdown syntax: **bold** *italic* [link](https://example.com].' initialValues={{ description: initialValue }} submitHandler={async ({ description }) => { await updateOneAreaCmd({ description }) @@ -30,9 +32,7 @@ export const AreaDescriptionForm: React.FC<{ initialValue: string, uuid: string diff --git a/src/app/editArea/[slug]/general/AreaLatLngForm.tsx b/src/app/editArea/[slug]/general/AreaLatLngForm.tsx index ea186c115..9bec8eeee 100644 --- a/src/app/editArea/[slug]/general/AreaLatLngForm.tsx +++ b/src/app/editArea/[slug]/general/AreaLatLngForm.tsx @@ -1,7 +1,7 @@ 'use client' import { useSession } from 'next-auth/react' -import { SingleEntryForm } from 'app/area/[...slug]/SingleEntryForm' +import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm' import { AREA_LATLNG_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm' import { DashboardInput } from '@/components/ui/form/Input' import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd' @@ -18,6 +18,8 @@ export const AreaLatLngForm: React.FC<{ initLat: number, initLng: number, uuid: return ( initialValues={{ latlngStr }} + title='Coordinates' + helperText='The location may be where the trail meets the wall or the midpoint of the wall.' submitHandler={({ latlngStr }) => { const latlng = parseLatLng(latlngStr) if (latlng != null) { @@ -29,9 +31,7 @@ export const AreaLatLngForm: React.FC<{ initLat: number, initLng: number, uuid: > diff --git a/src/app/editArea/[slug]/general/AreaNameForm.tsx b/src/app/editArea/[slug]/general/AreaNameForm.tsx index 53ff8988d..6dd8d12f0 100644 --- a/src/app/editArea/[slug]/general/AreaNameForm.tsx +++ b/src/app/editArea/[slug]/general/AreaNameForm.tsx @@ -1,9 +1,11 @@ 'use client' import { useSession } from 'next-auth/react' +import { ValidationValueMessage } from 'react-hook-form' -import { SingleEntryForm } from 'app/area/[...slug]/SingleEntryForm' +import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm' import { DashboardInput } from '@/components/ui/form/Input' import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd' +import { AREA_NAME_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm' export const AreaNameForm: React.FC<{ initialValue: string, uuid: string }> = ({ uuid, initialValue }) => { const session = useSession({ required: true }) @@ -12,19 +14,23 @@ export const AreaNameForm: React.FC<{ initialValue: string, uuid: string }> = ({ accessToken: session?.data?.accessToken as string } ) + + const maxLengthValidation = AREA_NAME_FORM_VALIDATION_RULES.maxLength as ValidationValueMessage + return ( + title='Area name' initialValues={{ areaName: initialValue }} submitHandler={async ({ areaName }) => { await updateOneAreaCmd({ areaName }) }} + helperText={`Please use ${maxLengthValidation.value.toString()} characters at maximum.`} > ) diff --git a/src/app/editArea/[slug]/general/page.tsx b/src/app/editArea/[slug]/general/page.tsx new file mode 100644 index 000000000..dc8093939 --- /dev/null +++ b/src/app/editArea/[slug]/general/page.tsx @@ -0,0 +1,46 @@ +import { notFound } from 'next/navigation' +import { validate } from 'uuid' +import { ReactNode } from 'react' + +import { AreaPageDataProps, getArea } from '@/js/graphql/getArea' +import { AreaNameForm } from './AreaNameForm' +import { AreaDescriptionForm } from './AreaDescriptionForm' + +// Opt out of caching for all data requests in the route segment +export const dynamic = 'force-dynamic' + +export interface DashboardPageProps { + params: { + slug: string + } +} + +export default async function AreaEditPage ({ params }: DashboardPageProps): Promise { + const { area: { areaName, uuid, content: { description } } } = await getPageDataForEdit(params.slug) + return ( + + + + + ) +} + +export const PageContainer: React.FC<{ children: ReactNode } > = ({ children }) => ( +
+ {children} +
+) + +export const getPageDataForEdit = async (pageSlug: string): Promise => { + if (pageSlug == null) notFound() + + if (!validate(pageSlug)) { + notFound() + } + + const pageData = await getArea(pageSlug) + if (pageData == null) { + notFound() + } + return pageData +} diff --git a/src/app/editArea/[slug]/layout.tsx b/src/app/editArea/[slug]/layout.tsx index 316b57d6b..01169b64b 100644 --- a/src/app/editArea/[slug]/layout.tsx +++ b/src/app/editArea/[slug]/layout.tsx @@ -10,9 +10,9 @@ export default function RootLayout ({

Edit area


-
+
-
+
{children}
diff --git a/src/app/editArea/[slug]/loading.tsx b/src/app/editArea/[slug]/loading.tsx index 1ffe0ce3d..d28e1a741 100644 --- a/src/app/editArea/[slug]/loading.tsx +++ b/src/app/editArea/[slug]/loading.tsx @@ -1,11 +1,11 @@ -import { PageContainer } from './page' +import { PageContainer } from './general/page' export default function Loading (): JSX.Element { return ( -
-
-
+
+
+
) } diff --git a/src/app/editArea/[slug]/attributes/page.tsx b/src/app/editArea/[slug]/location/page.tsx similarity index 90% rename from src/app/editArea/[slug]/attributes/page.tsx rename to src/app/editArea/[slug]/location/page.tsx index 6fde48829..3d6d68a1e 100644 --- a/src/app/editArea/[slug]/attributes/page.tsx +++ b/src/app/editArea/[slug]/location/page.tsx @@ -1,10 +1,9 @@ import { AreaLatLngForm } from '../general/AreaLatLngForm' -import { DashboardPageProps, getPageDataForEdit, PageContainer } from '../page' +import { DashboardPageProps, getPageDataForEdit, PageContainer } from '../general/page' export default async function AreaEditPage ({ params }: DashboardPageProps): Promise { const { area: { uuid, metadata: { lat, lng } } } = await getPageDataForEdit(params.slug) - console.log('#', lat, lng) return ( diff --git a/src/app/editArea/[slug]/manage/AddAreaForm.tsx b/src/app/editArea/[slug]/manage/AddAreaForm.tsx new file mode 100644 index 000000000..354a8d8d3 --- /dev/null +++ b/src/app/editArea/[slug]/manage/AddAreaForm.tsx @@ -0,0 +1,46 @@ +'use client' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/navigation' + +import { SingleEntryForm } from 'app/editArea/[slug]/components/SingleEntryForm' +import { DashboardInput } from '@/components/ui/form/Input' +import useUpdateAreasCmd from '@/js/hooks/useUpdateAreasCmd' +import { AreaType } from '@/js/types' +import { AreaDesignationRadioGroup, areaDesignationToDb, AreaTypeFormProp } from '@/components/edit/form/AreaDesignationRadioGroup' +import { AREA_NAME_FORM_VALIDATION_RULES } from '@/components/edit/EditAreaForm' + +/** + * + */ +export const AddAreaForm: React.FC<{ area: AreaType }> = ({ area }) => { + const { uuid } = area + const session = useSession({ required: true }) + const router = useRouter() + const { addOneAreaCmd } = useUpdateAreasCmd({ + areaId: uuid, + accessToken: session?.data?.accessToken as string + } + ) + return ( + + initialValues={{ areaName: '' }} + keepValuesAfterReset={false} + title='Add new area' + helperText='Do not copy description from guidebooks.' + submitHandler={async ({ areaName, areaType }) => { + const { isBoulder, isLeaf } = areaDesignationToDb(areaType) + await addOneAreaCmd({ name: areaName, parentUuid: uuid, isBoulder, isLeaf }) + router.refresh() // Ask Next to refresh props from the server + }} + className='border-primary border-2' + > + + + + ) +} diff --git a/src/app/editArea/[slug]/manage/components/AreaItem.tsx b/src/app/editArea/[slug]/manage/components/AreaItem.tsx new file mode 100644 index 000000000..fa41b2f63 --- /dev/null +++ b/src/app/editArea/[slug]/manage/components/AreaItem.tsx @@ -0,0 +1,76 @@ +import { forwardRef } from 'react' +import Link from 'next/link' +import { Graph, ShareNetwork, LineSegments, Cube } from '@phosphor-icons/react/dist/ssr' +import { Icon, IconProps } from '@phosphor-icons/react' + +import { AreaType } from '@/js/types' +import { DeleteAreaTrigger, DeleteAreaTriggerButtonSm } from '@/components/edit/Triggers' + +type EType = 'area' | 'crag' | 'boulder' | 'climb' + +const CragIcon = forwardRef((props, ref) => ) + +const IconMap: Record = { + area: Graph, + crag: CragIcon, + boulder: Cube, + climb: LineSegments +} + +export const EntityIcon: React.FC<{ type: EType, withLabel?: boolean }> = ({ type, withLabel = true }) => { + const IconComponent = IconMap?.[type] + if (IconComponent == null) return null + return ( +
+ + {type.toUpperCase()} +
+ ) +} + +export const AreaItem: React.FC<{ area: AreaType, parentUuid: string, index: number }> = ({ area, index, parentUuid }) => { + const { uuid, areaName } = area + return ( +
+
+
{index}
+
+
{areaName}
+ +
+ +
+
+ ) +} + +export const AreaIcon: React.FC<{ area: AreaType }> = ({ area: { climbs, metadata: { isBoulder } } }) => { + if ((climbs?.length ?? 0) > 0) { + return + } + if (isBoulder) { + return + } + return +} + +const Actions: React.FC<{ uuid: string, areaName: string, parentUuid: string }> = ({ uuid, parentUuid, areaName }) => { + return ( +
+ + + + + + + + + +
+ ) +} diff --git a/src/app/editArea/[slug]/manage/components/AreaList.tsx b/src/app/editArea/[slug]/manage/components/AreaList.tsx new file mode 100644 index 000000000..4502393b0 --- /dev/null +++ b/src/app/editArea/[slug]/manage/components/AreaList.tsx @@ -0,0 +1,31 @@ +'use client' +import { AreaType } from '@/js/types' +import { AreaItem } from './AreaItem' + +type AreaListProps = Pick & { + areas: AreaType[] +} + +export const AreaList: React.FC = ({ areaName, uuid, pathTokens, ancestors, areas }) => { + return ( +
+ +
+
+ +
+ {areas.map((item, index) => + )} +
+
+
+ {/* */} + +
+ ) +} diff --git a/src/app/editArea/[slug]/manage/page.tsx b/src/app/editArea/[slug]/manage/page.tsx new file mode 100644 index 000000000..c5f291801 --- /dev/null +++ b/src/app/editArea/[slug]/manage/page.tsx @@ -0,0 +1,28 @@ +import { DashboardPageProps, getPageDataForEdit, PageContainer } from '../general/page' +import { AreaList } from './components/AreaList' +import { AddAreaForm } from './AddAreaForm' + +// Opt out of caching for all data requests in the route segment +export const dynamic = 'force-dynamic' + +/** + * Area management + */ +export default async function Page ({ params }: DashboardPageProps): Promise { + const { area } = await getPageDataForEdit(params.slug) + + const { areaName, uuid, ancestors, pathTokens, children } = area + + return ( + + + + + ) +} diff --git a/src/app/editArea/[slug]/page.tsx b/src/app/editArea/[slug]/page.tsx index 7f7b3f3aa..7f48aa17f 100644 --- a/src/app/editArea/[slug]/page.tsx +++ b/src/app/editArea/[slug]/page.tsx @@ -1,12 +1,5 @@ -import { notFound } from 'next/navigation' -import { validate } from 'uuid' -import { ReactNode } from 'react' +import { redirect, notFound } from 'next/navigation' -import { AreaPageDataProps, getArea } from '@/js/graphql/getArea' -import { AreaNameForm } from './general/AreaNameForm' -import { AreaDescriptionForm } from './general/AreaDescriptionForm' - -// Opt out of caching for all data requests in the route segment export const dynamic = 'force-dynamic' export interface DashboardPageProps { @@ -16,31 +9,8 @@ export interface DashboardPageProps { } export default async function AreaEditPage ({ params }: DashboardPageProps): Promise { - const { area: { areaName, uuid, content: { description } } } = await getPageDataForEdit(params.slug) - return ( - - - - - ) -} - -export const PageContainer: React.FC<{ children: ReactNode } > = ({ children }) => ( -
- {children} -
-) - -export const getPageDataForEdit = async (pageSlug: string): Promise => { - if (pageSlug == null) notFound() - - if (!validate(pageSlug)) { - notFound() - } - - const pageData = await getArea(pageSlug) - if (pageData == null) { + if (params.slug == null) { notFound() } - return pageData + redirect(`/editArea/${params.slug}/general`) } diff --git a/src/app/editArea/[slug]/updateCache/route.ts b/src/app/editArea/[slug]/updateCache/route.ts new file mode 100644 index 000000000..5225e8bf1 --- /dev/null +++ b/src/app/editArea/[slug]/updateCache/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' +import { revalidatePath } from 'next/cache' + +export async function GET (request: Request, res: NextResponse): Promise { + revalidatePath('/editArea/[slug]', 'page') + return NextResponse.json({ message: 'ok' }) +} diff --git a/src/app/global.css b/src/app/global.css index f6390e023..85710f51a 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -14,16 +14,6 @@ html { @apply font-sans text-base text-base-content bg-base-100; } -/* .btn { - min-height: 2rem; - height: 2rem; -} - -.btn-md { - min-height: 2rem; - height: 2rem; -} */ - h1 { @apply text-base-content text-4xl lg:text-5xl tracking-tight; } @@ -36,10 +26,18 @@ h3 { @apply text-base-content text-lg font-semibold tracking-tight leading-loose; } -.card-body { - @apply sm:px-0 border-0 !important; +.text-primary { + @apply text-base-content; } +.text-secondary { + @apply text-base-content/60; +} + +/* .card-body { + @apply sm:px-0 border-0 !important; +} */ + /* Layout for summary section of the area and climb page. 2 columns on desktop, normal stack div on mobile. */ .area-climb-page-summary { @apply mt-6 lg:grid lg:grid-cols-3 w-full; @@ -72,3 +70,21 @@ h3 { pointer-events: none; @apply top-4 left-4; } + +/** 2-column layout for Area and Climb list */ +.two-column-table { + @apply lg:gap-x-24 lg:columns-2; +} + +.area-row { + @apply py-4 flex flex-row flex-nowrap gap-4 px-4 items-start border-t break-inside-avoid-column break-inside-avoid; +} + +.area-entity-box { + box-shadow: 2px 2px 0px #fe4f1a; + @apply rounded h-8 w-8 grid place-content-center bg-area-cue text-base-100 text-sm hover:decoration-0 hover:no-underline; +} + +.thick-link { + @apply hover:underline underline-offset-4 decoration-4; +} diff --git a/src/components/edit/AddChildAreaForm.tsx b/src/components/edit/AddChildAreaForm.tsx index c6e8890ab..6d719dd51 100644 --- a/src/components/edit/AddChildAreaForm.tsx +++ b/src/components/edit/AddChildAreaForm.tsx @@ -21,6 +21,7 @@ type ProgressState = 'initial' | 'data-entry' | 'confirm' /** * Add child area wizard. Users must be authenticated. + * @deprecated Replaced by the new editArea/[slug] dashboard */ export default function AddAreaForm ({ parentName, parentUuid, onSuccess }: AddAreaFormProps): JSX.Element { const session = useSession() diff --git a/src/components/edit/AreaCRUD.tsx b/src/components/edit/AreaCRUD.tsx index 9adba80d7..d817bca5d 100644 --- a/src/components/edit/AreaCRUD.tsx +++ b/src/components/edit/AreaCRUD.tsx @@ -1,3 +1,5 @@ +'use client' +import dynamic from 'next/dynamic' import type { MouseEvent } from 'react' import clx from 'classnames' import { @@ -31,7 +33,7 @@ import { AreaType } from '../../js/types' export type AreaCRUDProps = Pick & { childAreas: AreaType[] editMode: boolean - onChange: () => void + onChange?: () => void } /** @@ -173,7 +175,7 @@ type AreaItemProps = AreaSummaryType & { borderBottom: boolean parentUuid: string editMode?: boolean - onChange: () => void + onChange?: () => void } /** @@ -247,3 +249,5 @@ function shouldHandleEvent (element: HTMLElement | null): boolean { return true } + +export const DynamicAreaCRUD = dynamic(async () => await Promise.resolve(AreaCRUD)) diff --git a/src/components/edit/DeleteAreaForm.tsx b/src/components/edit/DeleteAreaForm.tsx index db8ce0a33..256da6ec1 100644 --- a/src/components/edit/DeleteAreaForm.tsx +++ b/src/components/edit/DeleteAreaForm.tsx @@ -1,12 +1,12 @@ +'use client' import { useEffect } from 'react' import { useForm, FormProvider } from 'react-hook-form' -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import clx from 'classnames' import { GraphQLError } from 'graphql' import { signIn, useSession } from 'next-auth/react' import useUpdateAreasCmd from '../../js/hooks/useUpdateAreasCmd' import Input from '../ui/form/Input' - export interface DeleteAreaProps { parentUuid: string areaUuid: string @@ -44,7 +44,6 @@ export default function DeleteAreaForm ({ areaUuid, areaName, parentUuid, return } if (returnToParentPageAfterDelete) { void router.replace('/crag/' + parentUuid) - router.reload() } } diff --git a/src/components/edit/EditAreaForm.tsx b/src/components/edit/EditAreaForm.tsx index 33257a50b..160b54bf9 100644 --- a/src/components/edit/EditAreaForm.tsx +++ b/src/components/edit/EditAreaForm.tsx @@ -50,6 +50,9 @@ interface HtmlFormProps extends AreaUpdatableFieldsType { areaType: AreaTypeFormProp } +/** + * @deprecated replaced by the new editArea page + */ export default function AreaEditForm (props: AreaType & { formRef?: any }): JSX.Element { const { uuid, areaName, shortCode, pathTokens, content: { description }, children, climbs, metadata, formRef } = props const { lat, lng } = metadata diff --git a/src/components/edit/SortableItem.tsx b/src/components/edit/SortableItem.tsx index c9629baf5..f7430594f 100644 --- a/src/components/edit/SortableItem.tsx +++ b/src/components/edit/SortableItem.tsx @@ -4,10 +4,10 @@ import { useSortable } from '@dnd-kit/sortable' interface SortableItemProps { id: string children: JSX.Element - disabled: boolean + disabled?: boolean } -export const SortableItem = (props: SortableItemProps): JSX.Element => { +export const SortableItem = ({ id, disabled = false, children }: SortableItemProps): JSX.Element => { const { attributes, listeners, @@ -15,7 +15,7 @@ export const SortableItem = (props: SortableItemProps): JSX.Element => { transform, transition, isDragging - } = useSortable({ id: props.id, disabled: props.disabled }) + } = useSortable({ id, disabled }) const style = { transform: CSS.Transform.toString(transform), @@ -25,7 +25,7 @@ export const SortableItem = (props: SortableItemProps): JSX.Element => { return (
- {props.children} + {children}
) } diff --git a/src/components/edit/Triggers.tsx b/src/components/edit/Triggers.tsx index e2fe7e48a..f82c748d4 100644 --- a/src/components/edit/Triggers.tsx +++ b/src/components/edit/Triggers.tsx @@ -1,6 +1,7 @@ +'use client' import { useState, useCallback } from 'react' +import { Trash } from '@phosphor-icons/react/dist/ssr' import { PlusIcon, PlusCircleIcon } from '@heroicons/react/20/solid' -import { TrashIcon } from '@heroicons/react/24/outline' import { MobileDialog, DialogContent, DialogTrigger } from '../ui/MobileDialog' import DeleteAreaForm, { DeleteAreaProps } from './DeleteAreaForm' import AddAreaForm, { AddAreaFormProps } from './AddChildAreaForm' @@ -72,18 +73,20 @@ export const DeleteAreaTriggerButtonSm = ({ disabled }: TriggerButtonProps): JSX -
- +
+
) : ( - + ) ) diff --git a/src/components/edit/form/AreaDesignationRadioGroup.tsx b/src/components/edit/form/AreaDesignationRadioGroup.tsx index 1453887a9..559f684b7 100644 --- a/src/components/edit/form/AreaDesignationRadioGroup.tsx +++ b/src/components/edit/form/AreaDesignationRadioGroup.tsx @@ -27,7 +27,8 @@ export const AreaDesignationRadioGroup = ({ name = 'areaType', disabled = false values={['area', 'crag', 'boulder']} labelTips={['Group other areas.', 'List rope climbing routes.', 'List boulder problems.']} requiredErrorMessage='Please select an area type' - />) + /> +) export const ExplainAreaTypeLockTooltip = ({ canEdit }: { canEdit: boolean }): JSX.Element | null => ( diff --git a/src/components/ui/Card/Card.tsx b/src/components/ui/Card/Card.tsx index 3ed9eb6ba..d301b6ac5 100644 --- a/src/components/ui/Card/Card.tsx +++ b/src/components/ui/Card/Card.tsx @@ -23,7 +23,7 @@ export default function Card ({ {image} {imageActions} -
{body}
+
{body}
) } diff --git a/src/components/ui/MobileDialog.tsx b/src/components/ui/MobileDialog.tsx index e6eece3a9..af36995be 100644 --- a/src/components/ui/MobileDialog.tsx +++ b/src/components/ui/MobileDialog.tsx @@ -76,7 +76,7 @@ export const MobileDialog = DialogPrimitive.Root /** * A button used to trigger the dialog. */ -export const DialogTrigger = React.forwardRef((props, forwardedRef) => +export const DialogTrigger = React.forwardRef((props, forwardedRef) => ) /** diff --git a/src/components/ui/form/Input.tsx b/src/components/ui/form/Input.tsx index cedf1e963..ae5e22ad6 100644 --- a/src/components/ui/form/Input.tsx +++ b/src/components/ui/form/Input.tsx @@ -1,6 +1,5 @@ import { RegisterOptions, useFormContext, UseFormReturn } from 'react-hook-form' import clx from 'classnames' -import { SpinnerGap } from '@phosphor-icons/react/dist/ssr' interface InputProps { label?: string @@ -106,8 +105,6 @@ const AFFIX_DEFAULT_CSS = 'bg-default uppercase text-sm' export interface DashboardInputProps { name: string label: string - description: string - helper: string placeholder?: string disabled?: boolean readOnly?: boolean @@ -118,54 +115,32 @@ export interface DashboardInputProps { } -export const DashboardInput: React.FC = ({ name, label, description, helper, placeholder, disabled = false, readOnly = false, registerOptions, type = 'text', spellCheck = false, className = '' }) => { +export const DashboardInput: React.FC = ({ name, label, placeholder, disabled = false, readOnly = false, registerOptions, type = 'text', spellCheck = false, className = '' }) => { const formContext = useFormContext() - const { formState: { errors, isValid, isSubmitting, isDirty } } = formContext + const { formState: { errors } } = formContext const error = errors?.[name] return ( -
-
-
- - -
- {/* Footer */} -
- - -
-
+
+ + + {error?.message != null && + }
) } - -export const SubmitButton: React.FC<{ isValid: boolean, isSubmitting: boolean, isDirty: boolean }> = ({ - isValid, isSubmitting, isDirty -}) => ( - -) diff --git a/src/components/ui/form/MDTextArea.tsx b/src/components/ui/form/MDTextArea.tsx index 3d2b03f95..00105ea50 100644 --- a/src/components/ui/form/MDTextArea.tsx +++ b/src/components/ui/form/MDTextArea.tsx @@ -6,15 +6,12 @@ import dynamic from 'next/dynamic' import { RulesType } from '@/js/types' import { MarkdownEditorProps } from '@/components/editor/MarkdownEditor' -import { SubmitButton } from './Input' interface EditorProps { initialValue?: string editable?: boolean name: string label: string - description: string - helper: string placeholder?: string rules?: RulesType } @@ -22,46 +19,38 @@ interface EditorProps { /** * Multiline inplace editor with react-hook-form support. */ -export const MDTextArea: React.FC = ({ initialValue = '', name, placeholder = 'Enter some text', label, description, helper, rules }) => { - const { fieldState: { error }, formState: { isValid, isDirty, isSubmitting } } = useController({ name, rules }) +export const MDTextArea: React.FC = ({ initialValue = '', name, placeholder = 'Enter some text', label, rules }) => { + const { fieldState: { error } } = useController({ name, rules }) const [preview, setPreview] = useState(false) return ( -
-
-
- + - {/* Footer */} -
- - -
-
+ {error?.message != null && + }
) } -export const MarkdownEditor = dynamic(async () => await import('@/components/editor/MarkdownEditor').then( +/** + * Lazy load to avoid Next hydration issue. + */ +export const LazyMarkdownEditor = dynamic(async () => await import('@/components/editor/MarkdownEditor').then( module => module.MarkdownEditor), { ssr: true -} -) +}) diff --git a/src/components/ui/form/RadioGroup.tsx b/src/components/ui/form/RadioGroup.tsx index 6d3092737..ae309e1b5 100644 --- a/src/components/ui/form/RadioGroup.tsx +++ b/src/components/ui/form/RadioGroup.tsx @@ -54,7 +54,7 @@ export default function RadioGroup ({ groupLabel, groupLabelAlt, name, labels, v ))}
-