Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,893 changes: 310 additions & 1,583 deletions app/user/page.tsx

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions components/profile/image-customization-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import type { ReactNode } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline'

interface ImageCustomizationModalProps {
open: boolean
title: string
onClose: () => void
children: ReactNode
}

/** The plain overlay + wide panel the avatar and banner editors sit in. */
export function ImageCustomizationModal({ open, title, onClose, children }: ImageCustomizationModalProps) {
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-neutral-900 rounded-xl p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">{title}</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full" aria-label="Close">
<XMarkIcon className="w-5 h-5" />
</button>
</div>
{children}
</div>
</div>
)
}
107 changes: 107 additions & 0 deletions components/profile/profile-edit-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use client'

import type { SocialLink } from '@/lib/types'
import { PaymentUriInput } from '@/components/profile/payment-uri-input'
import { SocialLinksInput } from '@/components/profile/social-links-input'

export interface ProfileDraft {
displayName: string
bio: string
location: string
website: string
pronouns: string
nsfw: boolean
paymentUris: string[]
socialLinks: SocialLink[]
}

export const EMPTY_DRAFT: ProfileDraft = {
displayName: '',
bio: '',
location: '',
website: '',
pronouns: '',
nsfw: false,
paymentUris: [],
socialLinks: [],
}

interface ProfileEditFormProps {
draft: ProfileDraft
onChange: (draft: ProfileDraft) => void
disabled: boolean
}

const INPUT =
'mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-transparent focus:outline-none focus:ring-2 focus:ring-yappr-500'

function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">{label}</label>
{children}
</div>
)
}

/** The in-place editor for your own profile; the page owns the draft and saves it. */
export function ProfileEditForm({ draft, onChange, disabled }: ProfileEditFormProps) {
const set = <K extends keyof ProfileDraft>(key: K, value: ProfileDraft[K]) => onChange({ ...draft, [key]: value })
return (
<div className="space-y-4">
<Field label="Name">
<input type="text" value={draft.displayName} onChange={(e) => set('displayName', e.target.value)} className={INPUT} maxLength={50} />
</Field>
<Field label="Pronouns">
<input
type="text"
value={draft.pronouns}
onChange={(e) => set('pronouns', e.target.value)}
placeholder="e.g. she/her"
className={INPUT}
maxLength={20}
/>
</Field>
<Field label="Bio">
<textarea value={draft.bio} onChange={(e) => set('bio', e.target.value)} className={`${INPUT} resize-none`} rows={3} maxLength={160} />
<p className="text-xs text-gray-500 mt-1">{draft.bio.length}/160</p>
</Field>
<Field label="Location">
<input type="text" value={draft.location} onChange={(e) => set('location', e.target.value)} className={INPUT} maxLength={50} />
</Field>
<Field label="Website">
<input
type="text"
value={draft.website}
onChange={(e) => set('website', e.target.value)}
placeholder="https://example.com"
className={INPUT}
maxLength={200}
/>
</Field>

<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<PaymentUriInput uris={draft.paymentUris} onChange={(uris) => set('paymentUris', uris)} disabled={disabled} />
</div>

<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<SocialLinksInput links={draft.socialLinks} onChange={(links) => set('socialLinks', links)} disabled={disabled} />
</div>

<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={draft.nsfw}
onChange={(e) => set('nsfw', e.target.checked)}
className="w-4 h-4 text-yappr-500 rounded focus:ring-yappr-500"
/>
<div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">NSFW Content</span>
<p className="text-xs text-gray-500">Mark your profile as containing adult content</p>
</div>
</label>
</div>
</div>
)
}
Loading
Loading