mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-07 00:04:40 -04:00
Enhance message composer: add minRows support, character count validation, and dynamic placeholder customization
This commit is contained in:
@@ -494,6 +494,12 @@
|
||||
"messages.you_prefix": "Sie: ",
|
||||
"messaging.email_verification_required": "Sie müssen Ihre E-Mail überprüfen, um Personen zu schreiben.",
|
||||
"messaging.send_thoughtful_message": "Sende ihnen eine durchdachte Nachricht",
|
||||
"send_message.placeholder": "Was hat dich an {name}s Profil wirklich angesprochen? Was möchtest du gemeinsam erkunden?",
|
||||
"send_message.title": "Beginne ein bedeutungsvolles Gespräch",
|
||||
"send_message.guidance": "Compass geht um Tiefe. Nimm dir einen Moment, um etwas Echtes zu schreiben.",
|
||||
"send_message.keywords_hint": "Füge einige von {name}s Themen in deine Nachricht ein",
|
||||
"send_message.more_chars": "{count} Zeichen mehr",
|
||||
"send_message.ready": "Bereit zu senden",
|
||||
"more_options_user.ban_user": "Benutzer sperren",
|
||||
"more_options_user.banned": "Gesperrt",
|
||||
"more_options_user.banning": "Sperre...",
|
||||
@@ -1130,7 +1136,6 @@
|
||||
"security.seo.title": "Sicherheit",
|
||||
"security.title": "Sicherheit",
|
||||
"send_message.button_label": "Nachricht",
|
||||
"send_message.title": "Kontakt",
|
||||
"settings.action.cancel": "Abbrechen",
|
||||
"settings.action.save": "Speichern",
|
||||
"settings.connection_preferences.description": "Kontrollieren Sie, wie andere sich mit Ihnen verbinden können.",
|
||||
|
||||
@@ -494,6 +494,12 @@
|
||||
"messages.you_prefix": "Vous : ",
|
||||
"messaging.email_verification_required": "Vous devez vérifier votre e-mail pour pouvoir envoyer des messages.",
|
||||
"messaging.send_thoughtful_message": "Envoyez-leur un message réfléchi",
|
||||
"send_message.placeholder": "Qu'est-ce qui vous a vraiment touché dans le profil de {name} ? Qu'aimeriez-vous explorer ensemble ?",
|
||||
"send_message.title": "Commencez une conversation qui a du sens",
|
||||
"send_message.guidance": "Prenez un moment pour écrire quelque chose d'authentique.",
|
||||
"send_message.keywords_hint": "Insérez quelques-uns des sujets de {name} dans votre message",
|
||||
"send_message.more_chars": "{count} caractères de plus",
|
||||
"send_message.ready": "Prêt à envoyer",
|
||||
"more_options_user.ban_user": "Bannir l'utilisateur",
|
||||
"more_options_user.banned": "Banni",
|
||||
"more_options_user.banning": "Bannissement...",
|
||||
@@ -1129,7 +1135,6 @@
|
||||
"security.seo.title": "Sécurité",
|
||||
"security.title": "Sécurité",
|
||||
"send_message.button_label": "Contacter",
|
||||
"send_message.title": "Contactez",
|
||||
"settings.action.cancel": "Annuler",
|
||||
"settings.action.save": "Enregistrer",
|
||||
"settings.connection_preferences.description": "Contrôlez comment les autres peuvent se connecter avec vous.",
|
||||
|
||||
@@ -131,6 +131,7 @@ export function CommentInputTextArea(props: {
|
||||
cancelEditing?: () => void
|
||||
isSubmitting: boolean
|
||||
submitOnEnter?: boolean
|
||||
isDisabled?: boolean
|
||||
isEditing?: boolean
|
||||
commentTypes?: CommentType[]
|
||||
}) {
|
||||
@@ -144,6 +145,7 @@ export function CommentInputTextArea(props: {
|
||||
replyTo,
|
||||
commentTypes = ['comment'],
|
||||
cancelEditing,
|
||||
isDisabled,
|
||||
} = props
|
||||
const t = useT()
|
||||
|
||||
@@ -200,7 +202,7 @@ export function CommentInputTextArea(props: {
|
||||
{user && !isSubmitting && submit && commentTypes.includes('repost') && (
|
||||
<Tooltip text={'Post question & comment to your followers'} className={'mt-2'}>
|
||||
<button
|
||||
disabled={!editor || editor.isEmpty}
|
||||
disabled={isDisabled || !editor || editor.isEmpty}
|
||||
className="text-ink-500 hover:text-ink-700 active:bg-ink-300 disabled:text-ink-300 px-2 transition-colors"
|
||||
onClick={() => submit('repost')}
|
||||
>
|
||||
@@ -220,10 +222,10 @@ export function CommentInputTextArea(props: {
|
||||
</button>
|
||||
</Row>
|
||||
)}
|
||||
{!isSubmitting && submit && commentTypes.includes('comment') && (
|
||||
{!isSubmitting && !isDisabled && submit && commentTypes.includes('comment') && (
|
||||
<button
|
||||
className="text-ink-500 hover:text-ink-700 active:bg-ink-300 disabled:text-ink-300 px-4 transition-colors"
|
||||
disabled={!editor || editor.isEmpty}
|
||||
disabled={isDisabled || !editor || editor.isEmpty}
|
||||
onClick={() => submit('comment')}
|
||||
>
|
||||
<PaperAirplaneIcon className="m-0 h-[25px] w-[22px] p-0" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import clsx from 'clsx'
|
||||
import {MAX_COMMENT_LENGTH} from 'common/comment'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {User} from 'common/user'
|
||||
import {findKey} from 'lodash'
|
||||
import {useRouter} from 'next/router'
|
||||
@@ -9,10 +10,8 @@ import {Button} from 'web/components/buttons/button'
|
||||
import {CommentInputTextArea} from 'web/components/comments/comment-input'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Modal, MODAL_CLASS} from 'web/components/layout/modal'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {EmailVerificationPrompt} from 'web/components/messaging/email-verification-prompt'
|
||||
import {useTextEditor} from 'web/components/widgets/editor'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {Tooltip} from 'web/components/widgets/tooltip'
|
||||
import {useFirebaseUser} from 'web/hooks/use-firebase-user'
|
||||
import {useSortedPrivateMessageMemberships} from 'web/hooks/use-private-messages'
|
||||
@@ -24,13 +23,15 @@ import {useT} from 'web/lib/locale'
|
||||
export const SendMessageButton = (props: {
|
||||
toUser: User
|
||||
currentUser: User | undefined | null
|
||||
profile: Profile
|
||||
includeLabel?: boolean
|
||||
circleButton?: boolean
|
||||
text?: string
|
||||
tooltipText?: string
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
const {toUser, currentUser, includeLabel, circleButton, text, tooltipText, disabled} = props
|
||||
const {toUser, currentUser, profile, includeLabel, circleButton, text, tooltipText, disabled} =
|
||||
props
|
||||
const firebaseUser = useFirebaseUser()
|
||||
const router = useRouter()
|
||||
const privateUser = usePrivateUser()
|
||||
@@ -60,7 +61,13 @@ export const SendMessageButton = (props: {
|
||||
key: `compose-new-message-${toUser.id}`,
|
||||
size: 'sm',
|
||||
max: MAX_COMMENT_LENGTH,
|
||||
// placeholder: t('send_message.placeholder', '...'),
|
||||
placeholder: t(
|
||||
'send_message.placeholder',
|
||||
`What genuinely resonated with you in {name}'s profile? What would you like to explore together?`,
|
||||
{name: toUser.name},
|
||||
),
|
||||
className: 'min-h-[150px]',
|
||||
nRowsMin: 3,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,6 +104,36 @@ export const SendMessageButton = (props: {
|
||||
router.push(`/messages/${res.channelId}`)
|
||||
}
|
||||
|
||||
const [insertedChips, setInsertedChips] = useState<string[]>([])
|
||||
|
||||
const toggleChip = (label: string) => {
|
||||
if (!editor) return
|
||||
|
||||
const alreadyInserted = insertedChips.includes(label)
|
||||
|
||||
if (alreadyInserted) {
|
||||
// remove the token from the editor text
|
||||
const current = editor.getText()
|
||||
const cleaned = current.replace(new RegExp(`\\s?${label}`, 'gi'), '').trim()
|
||||
editor.commands.setContent(cleaned)
|
||||
setInsertedChips((prev) => prev.filter((c) => c !== label))
|
||||
} else {
|
||||
// append at cursor (or end)
|
||||
editor.chain().focus().insertContent(` ${label}`).run()
|
||||
setInsertedChips((prev) => [...prev, label])
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_CHARS = 200
|
||||
|
||||
const charCount = editor?.getText().trim().length ?? 0
|
||||
const pct = Math.min((charCount / MIN_CHARS) * 100, 100)
|
||||
// Smooth color transition from red (0%) to green (100%)
|
||||
const r = pct < 50 ? 255 : Math.round(((100 - pct) / 50) * 255)
|
||||
const g = pct < 50 ? Math.round((pct / 50) * 255) : 255
|
||||
const b = Math.round(0)
|
||||
const barColor = `rgb(${r}, ${g}, ${b})`
|
||||
|
||||
if (privateUser?.blockedByUserIds.includes(toUser.id)) return null
|
||||
|
||||
return (
|
||||
@@ -143,19 +180,78 @@ export const SendMessageButton = (props: {
|
||||
|
||||
<Modal open={openComposeModal} setOpen={setOpenComposeModal}>
|
||||
<Col className={MODAL_CLASS}>
|
||||
<Row className={'w-full'}>
|
||||
<Title className={'!mb-2'}>
|
||||
{t('send_message.title', 'Message')} {toUser.name}
|
||||
</Title>
|
||||
</Row>
|
||||
<Col className={'w-full'}>
|
||||
<p className={'!mb-2 text-xl font-bold'}>
|
||||
{t('send_message.title', 'Start a meaningful conversation')}
|
||||
</p>
|
||||
<p className={'guidance'}>
|
||||
{t(
|
||||
'send_message.guidance',
|
||||
'Compass is about depth. Take a moment to write something genuine.',
|
||||
)}
|
||||
</p>
|
||||
</Col>
|
||||
|
||||
{firebaseUser?.emailVerified ? (
|
||||
<CommentInputTextArea
|
||||
editor={editor}
|
||||
user={currentUser}
|
||||
submit={sendMessage}
|
||||
isSubmitting={!editor || submitting}
|
||||
submitOnEnter={false}
|
||||
/>
|
||||
<>
|
||||
{!!profile.keywords?.length && (
|
||||
<div className={'w-full border border-canvas-100 rounded-xl p-2'}>
|
||||
<p className={'text-ink-1000/55 mb-2 text-xs'}>
|
||||
{t(
|
||||
'send_message.keywords_hint',
|
||||
`Insert some of {name} topics in your message`,
|
||||
{name: toUser.name},
|
||||
)}
|
||||
</p>
|
||||
<div className={'flex flex-wrap gap-2'}>
|
||||
{profile.keywords.map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
type={'button'}
|
||||
onClick={() => toggleChip(k)}
|
||||
className={clsx(
|
||||
'text-xs px-3 py-1 rounded-full transition-colors',
|
||||
insertedChips.includes(k)
|
||||
? 'bg-primary-200 border border-primary-300'
|
||||
: 'bg-canvas-100 hover:bg-canvas-200',
|
||||
)}
|
||||
>
|
||||
{k}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CommentInputTextArea
|
||||
editor={editor}
|
||||
user={currentUser}
|
||||
submit={sendMessage}
|
||||
isSubmitting={!editor || submitting}
|
||||
isDisabled={charCount < MIN_CHARS}
|
||||
submitOnEnter={false}
|
||||
/>
|
||||
|
||||
{/* quality meter */}
|
||||
<div className={'mt-2 w-full flex items-center gap-3'}>
|
||||
<div
|
||||
className={
|
||||
'h-1 flex-1 rounded-full bg-canvas-100 border border-canvas-300 overflow-hidden'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={'h-full rounded-full transition-all duration-300'}
|
||||
style={{backgroundColor: barColor, width: `${pct}%`}}
|
||||
/>
|
||||
</div>
|
||||
<span className={'tabular-nums guidance shrink-0'}>
|
||||
{charCount < MIN_CHARS
|
||||
? t('send_message.more_chars', '{count} more characters', {
|
||||
count: MIN_CHARS - charCount,
|
||||
})
|
||||
: t('send_message.ready', 'Ready to send')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmailVerificationPrompt t={t} className="max-w-xl" />
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,6 @@ import {Input} from 'web/components/widgets/input'
|
||||
import {RadioToggleGroup} from 'web/components/widgets/radio-toggle-group'
|
||||
import {Select} from 'web/components/widgets/select'
|
||||
import {Slider} from 'web/components/widgets/slider'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {ChoiceMap, ChoiceSetter, useChoicesContext} from 'web/hooks/use-choices'
|
||||
import {api} from 'web/lib/api'
|
||||
import {useLocale, useT} from 'web/lib/locale'
|
||||
@@ -277,9 +276,13 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>{t('profile.optional.subtitle', 'Optional information')}</Title>
|
||||
|
||||
<Col className={'gap-8'}>
|
||||
<p className={'guidance'}>
|
||||
{t(
|
||||
'profile.optional.subtitle',
|
||||
'Although all the fields below are optional, they will help people better understand you and connect with you.',
|
||||
)}
|
||||
</p>
|
||||
<Category title={t('profile.llm.extract.title', 'Auto-fill')} className={'mt-0'} />
|
||||
<LLMExtractSection
|
||||
parsingEditor={parsingEditor}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
|
||||
<SendMessageButton
|
||||
toUser={user}
|
||||
currentUser={currentUser}
|
||||
profile={profile}
|
||||
text={t('messaging.send_thoughtful_message', 'Send them a thoughtful message')}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -221,6 +221,7 @@ export default function ProfileHeader(props: {
|
||||
<SendMessageButton
|
||||
toUser={user}
|
||||
currentUser={currentUser}
|
||||
profile={profile}
|
||||
tooltipText={tooltipText}
|
||||
disabled={!profile.allow_direct_messaging}
|
||||
/>
|
||||
|
||||
@@ -84,9 +84,11 @@ export function useTextEditor(props: {
|
||||
extensions?: Extensions
|
||||
className?: string
|
||||
onChange?: () => void
|
||||
nRowsMin?: number
|
||||
}) {
|
||||
const {placeholder, className, max, defaultValue, size = 'md', key, onChange} = props
|
||||
const {placeholder, className, max, defaultValue, size = 'md', key, onChange, nRowsMin} = props
|
||||
const simple = size === 'sm'
|
||||
const minRows = nRowsMin ?? (simple ? 2 : 3)
|
||||
|
||||
const [content, setContent] = usePersistentLocalState<JSONContent | undefined>(
|
||||
undefined,
|
||||
@@ -116,7 +118,7 @@ export function useTextEditor(props: {
|
||||
'dark:[&_.ProseMirror-gapcursor]:after:border-white', // gap cursor
|
||||
className,
|
||||
),
|
||||
style: `min-height: ${1 + 1.625 * (simple ? 2 : 3)}em`, // 1em padding + 1.625 lines per row
|
||||
style: `min-height: ${1 + 1.625 * minRows}em`, // 1em padding + 1.625 lines per row
|
||||
},
|
||||
})
|
||||
|
||||
@@ -149,7 +151,7 @@ export function useTextEditor(props: {
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass:
|
||||
'before:content-[attr(data-placeholder)] before:text-ink-500 before:float-left before:h-0 cursor-text',
|
||||
'before:content-[attr(data-placeholder)] before:text-ink-1000/55 before:text-sm before:float-left before:h-0 cursor-text',
|
||||
}),
|
||||
CharacterCount.configure({limit: max}),
|
||||
...(props.extensions ?? []),
|
||||
|
||||
@@ -1,167 +1,167 @@
|
||||
import {UserIcon} from '@heroicons/react/24/solid'
|
||||
import {LikeData, ShipData} from 'common/api/profile-types'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {keyBy, orderBy} from 'lodash'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {SendMessageButton} from 'web/components/messaging/send-message-button'
|
||||
import {Avatar, EmptyAvatar} from 'web/components/widgets/avatar'
|
||||
import {Carousel} from 'web/components/widgets/carousel'
|
||||
import {UserLink} from 'web/components/widgets/user-link'
|
||||
import {useProfileByUserId} from 'web/hooks/use-profile'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {useUserById} from 'web/hooks/use-user-supabase'
|
||||
|
||||
import {Subtitle} from './profile-subtitle'
|
||||
import {ShipsList} from './ships-display'
|
||||
|
||||
export const LikesDisplay = (props: {
|
||||
likesGiven: LikeData[]
|
||||
likesReceived: LikeData[]
|
||||
ships: ShipData[]
|
||||
refreshShips: () => Promise<void>
|
||||
profileProfile: Profile
|
||||
}) => {
|
||||
const {likesGiven, likesReceived, ships, refreshShips, profileProfile} = props
|
||||
|
||||
const likesGivenByUserId = keyBy(likesGiven, (l) => l.user_id)
|
||||
const likesReceivedByUserId = keyBy(likesReceived, (l) => l.user_id)
|
||||
const mutualLikeUserIds = Object.keys(likesGivenByUserId).filter(
|
||||
(userId) => likesReceivedByUserId[userId],
|
||||
)
|
||||
|
||||
const mutualLikes = mutualLikeUserIds.map((user_id) => {
|
||||
const likeGiven = likesGivenByUserId[user_id]
|
||||
const likeReceived = likesReceivedByUserId[user_id]
|
||||
const created_time = Math.max(likeGiven.created_time, likeReceived.created_time)
|
||||
return {user_id, created_time}
|
||||
})
|
||||
const sortedMutualLikes = orderBy(mutualLikes, 'created_time', 'desc')
|
||||
const onlyLikesGiven = likesGiven.filter((l) => !likesReceivedByUserId[l.user_id])
|
||||
const onlyLikesReceived = likesReceived.filter((l) => !likesGivenByUserId[l.user_id])
|
||||
|
||||
if (
|
||||
sortedMutualLikes.length === 0 &&
|
||||
onlyLikesReceived.length === 0 &&
|
||||
onlyLikesGiven.length === 0 &&
|
||||
ships.length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Col className="gap-4">
|
||||
{sortedMutualLikes.length > 0 && (
|
||||
<Col className="gap-2">
|
||||
<Subtitle>Mutual likes</Subtitle>
|
||||
<Carousel>
|
||||
{sortedMutualLikes.map((like) => {
|
||||
return (
|
||||
<MatchTile
|
||||
key={like.user_id}
|
||||
matchUserId={like.user_id}
|
||||
profileProfile={profileProfile}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Carousel>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{onlyLikesReceived.length > 0 && (
|
||||
<LikesList label="Likes received" likes={onlyLikesReceived} />
|
||||
)}
|
||||
{onlyLikesGiven.length > 0 && <LikesList label="Likes given" likes={onlyLikesGiven} />}
|
||||
{ships.length > 0 && (
|
||||
<ShipsList
|
||||
label="Shipped with"
|
||||
ships={ships}
|
||||
profileProfile={profileProfile}
|
||||
refreshShips={refreshShips}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const LikesList = (props: {label: string; likes: LikeData[]}) => {
|
||||
const {label, likes} = props
|
||||
|
||||
const maxShown = 50
|
||||
const truncatedLikes = likes.slice(0, maxShown)
|
||||
|
||||
return (
|
||||
<Col className="gap-1">
|
||||
<Subtitle>{label}</Subtitle>
|
||||
{truncatedLikes.length > 0 ? (
|
||||
<Carousel className="w-full" labelsParentClassName="gap-0">
|
||||
{truncatedLikes.map((like) => (
|
||||
<UserAvatar className="-ml-1 first:ml-0" key={like.user_id} userId={like.user_id} />
|
||||
))}
|
||||
</Carousel>
|
||||
) : (
|
||||
<div className="text-ink-500">None</div>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const UserAvatar = (props: {userId: string; className?: string}) => {
|
||||
const {userId, className} = props
|
||||
const profile = useProfileByUserId(userId)
|
||||
const user = useUserById(userId)
|
||||
|
||||
// console.debug('UserAvatar', user?.username, profile?.pinned_url)
|
||||
|
||||
if (!profile) return <EmptyAvatar className={className} size={10} />
|
||||
return <Avatar className={className} avatarUrl={profile.pinned_url} username={user?.username} />
|
||||
}
|
||||
|
||||
export const MatchTile = (props: {profileProfile: Profile; matchUserId: string}) => {
|
||||
const {matchUserId, profileProfile} = props
|
||||
const profile = useProfileByUserId(matchUserId)
|
||||
const user = useUserById(matchUserId)
|
||||
const currentUser = useUser()
|
||||
const isYourMatch = currentUser?.id === profileProfile.user_id
|
||||
|
||||
if (!profile || !user) return <Col className="mb-2 h-[184px] w-[200px] shrink-0"></Col>
|
||||
const {pinned_url} = profile
|
||||
|
||||
return (
|
||||
<Col className="mb-2 w-[200px] shrink-0 overflow-hidden rounded">
|
||||
<Col className="bg-canvas-0 w-full px-4 py-2">
|
||||
<UserLink
|
||||
className={
|
||||
'hover:text-primary-500 text-ink-1000 truncate font-semibold transition-colors'
|
||||
}
|
||||
user={user}
|
||||
hideBadge
|
||||
/>
|
||||
</Col>
|
||||
<Col className="relative h-36 w-full overflow-hidden">
|
||||
{pinned_url ? (
|
||||
<Link href={`/${user.username}`}>
|
||||
<Image
|
||||
src={pinned_url}
|
||||
// You must set these so we don't pay an extra $1k/month to vercel
|
||||
width={200}
|
||||
height={144}
|
||||
alt={`${user.username}`}
|
||||
className="h-36 w-full object-cover"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Col className="bg-ink-300 h-full w-full items-center justify-center">
|
||||
<UserIcon className="h-20 w-20" />
|
||||
</Col>
|
||||
)}
|
||||
{isYourMatch && (
|
||||
<Col className="absolute right-3 top-2 gap-2">
|
||||
<SendMessageButton toUser={user as any} currentUser={currentUser} circleButton />
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
// import {UserIcon} from '@heroicons/react/24/solid'
|
||||
// import {LikeData, ShipData} from 'common/api/profile-types'
|
||||
// import {Profile} from 'common/profiles/profile'
|
||||
// import {keyBy, orderBy} from 'lodash'
|
||||
// import Image from 'next/image'
|
||||
// import Link from 'next/link'
|
||||
// import {Col} from 'web/components/layout/col'
|
||||
// import {SendMessageButton} from 'web/components/messaging/send-message-button'
|
||||
// import {Avatar, EmptyAvatar} from 'web/components/widgets/avatar'
|
||||
// import {Carousel} from 'web/components/widgets/carousel'
|
||||
// import {UserLink} from 'web/components/widgets/user-link'
|
||||
// import {useProfileByUserId} from 'web/hooks/use-profile'
|
||||
// import {useUser} from 'web/hooks/use-user'
|
||||
// import {useUserById} from 'web/hooks/use-user-supabase'
|
||||
//
|
||||
// import {Subtitle} from './profile-subtitle'
|
||||
// import {ShipsList} from './ships-display'
|
||||
//
|
||||
// export const LikesDisplay = (props: {
|
||||
// likesGiven: LikeData[]
|
||||
// likesReceived: LikeData[]
|
||||
// ships: ShipData[]
|
||||
// refreshShips: () => Promise<void>
|
||||
// profileProfile: Profile
|
||||
// }) => {
|
||||
// const {likesGiven, likesReceived, ships, refreshShips, profileProfile} = props
|
||||
//
|
||||
// const likesGivenByUserId = keyBy(likesGiven, (l) => l.user_id)
|
||||
// const likesReceivedByUserId = keyBy(likesReceived, (l) => l.user_id)
|
||||
// const mutualLikeUserIds = Object.keys(likesGivenByUserId).filter(
|
||||
// (userId) => likesReceivedByUserId[userId],
|
||||
// )
|
||||
//
|
||||
// const mutualLikes = mutualLikeUserIds.map((user_id) => {
|
||||
// const likeGiven = likesGivenByUserId[user_id]
|
||||
// const likeReceived = likesReceivedByUserId[user_id]
|
||||
// const created_time = Math.max(likeGiven.created_time, likeReceived.created_time)
|
||||
// return {user_id, created_time}
|
||||
// })
|
||||
// const sortedMutualLikes = orderBy(mutualLikes, 'created_time', 'desc')
|
||||
// const onlyLikesGiven = likesGiven.filter((l) => !likesReceivedByUserId[l.user_id])
|
||||
// const onlyLikesReceived = likesReceived.filter((l) => !likesGivenByUserId[l.user_id])
|
||||
//
|
||||
// if (
|
||||
// sortedMutualLikes.length === 0 &&
|
||||
// onlyLikesReceived.length === 0 &&
|
||||
// onlyLikesGiven.length === 0 &&
|
||||
// ships.length === 0
|
||||
// ) {
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// return (
|
||||
// <Col className="gap-4">
|
||||
// {sortedMutualLikes.length > 0 && (
|
||||
// <Col className="gap-2">
|
||||
// <Subtitle>Mutual likes</Subtitle>
|
||||
// <Carousel>
|
||||
// {sortedMutualLikes.map((like) => {
|
||||
// return (
|
||||
// <MatchTile
|
||||
// key={like.user_id}
|
||||
// matchUserId={like.user_id}
|
||||
// profileProfile={profileProfile}
|
||||
// />
|
||||
// )
|
||||
// })}
|
||||
// </Carousel>
|
||||
// </Col>
|
||||
// )}
|
||||
//
|
||||
// {onlyLikesReceived.length > 0 && (
|
||||
// <LikesList label="Likes received" likes={onlyLikesReceived} />
|
||||
// )}
|
||||
// {onlyLikesGiven.length > 0 && <LikesList label="Likes given" likes={onlyLikesGiven} />}
|
||||
// {ships.length > 0 && (
|
||||
// <ShipsList
|
||||
// label="Shipped with"
|
||||
// ships={ships}
|
||||
// profileProfile={profileProfile}
|
||||
// refreshShips={refreshShips}
|
||||
// />
|
||||
// )}
|
||||
// </Col>
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// const LikesList = (props: {label: string; likes: LikeData[]}) => {
|
||||
// const {label, likes} = props
|
||||
//
|
||||
// const maxShown = 50
|
||||
// const truncatedLikes = likes.slice(0, maxShown)
|
||||
//
|
||||
// return (
|
||||
// <Col className="gap-1">
|
||||
// <Subtitle>{label}</Subtitle>
|
||||
// {truncatedLikes.length > 0 ? (
|
||||
// <Carousel className="w-full" labelsParentClassName="gap-0">
|
||||
// {truncatedLikes.map((like) => (
|
||||
// <UserAvatar className="-ml-1 first:ml-0" key={like.user_id} userId={like.user_id} />
|
||||
// ))}
|
||||
// </Carousel>
|
||||
// ) : (
|
||||
// <div className="text-ink-500">None</div>
|
||||
// )}
|
||||
// </Col>
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// const UserAvatar = (props: {userId: string; className?: string}) => {
|
||||
// const {userId, className} = props
|
||||
// const profile = useProfileByUserId(userId)
|
||||
// const user = useUserById(userId)
|
||||
//
|
||||
// // console.debug('UserAvatar', user?.username, profile?.pinned_url)
|
||||
//
|
||||
// if (!profile) return <EmptyAvatar className={className} size={10} />
|
||||
// return <Avatar className={className} avatarUrl={profile.pinned_url} username={user?.username} />
|
||||
// }
|
||||
//
|
||||
// export const MatchTile = (props: {profileProfile: Profile; matchUserId: string}) => {
|
||||
// const {matchUserId, profileProfile} = props
|
||||
// const profile = useProfileByUserId(matchUserId)
|
||||
// const user = useUserById(matchUserId)
|
||||
// const currentUser = useUser()
|
||||
// const isYourMatch = currentUser?.id === profileProfile.user_id
|
||||
//
|
||||
// if (!profile || !user) return <Col className="mb-2 h-[184px] w-[200px] shrink-0"></Col>
|
||||
// const {pinned_url} = profile
|
||||
//
|
||||
// return (
|
||||
// <Col className="mb-2 w-[200px] shrink-0 overflow-hidden rounded">
|
||||
// <Col className="bg-canvas-0 w-full px-4 py-2">
|
||||
// <UserLink
|
||||
// className={
|
||||
// 'hover:text-primary-500 text-ink-1000 truncate font-semibold transition-colors'
|
||||
// }
|
||||
// user={user}
|
||||
// hideBadge
|
||||
// />
|
||||
// </Col>
|
||||
// <Col className="relative h-36 w-full overflow-hidden">
|
||||
// {pinned_url ? (
|
||||
// <Link href={`/${user.username}`}>
|
||||
// <Image
|
||||
// src={pinned_url}
|
||||
// // You must set these so we don't pay an extra $1k/month to vercel
|
||||
// width={200}
|
||||
// height={144}
|
||||
// alt={`${user.username}`}
|
||||
// className="h-36 w-full object-cover"
|
||||
// />
|
||||
// </Link>
|
||||
// ) : (
|
||||
// <Col className="bg-ink-300 h-full w-full items-center justify-center">
|
||||
// <UserIcon className="h-20 w-20" />
|
||||
// </Col>
|
||||
// )}
|
||||
// {isYourMatch && (
|
||||
// <Col className="absolute right-3 top-2 gap-2">
|
||||
// <SendMessageButton toUser={user as any} currentUser={currentUser} circleButton />
|
||||
// </Col>
|
||||
// )}
|
||||
// </Col>
|
||||
// </Col>
|
||||
// )
|
||||
// }
|
||||
|
||||
6
web/lib/util/strings.ts
Normal file
6
web/lib/util/strings.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Returns the possessive form of a name.
|
||||
* Adds 's unless the name already ends with s, in which case just adds '.
|
||||
*/
|
||||
export const possessive = (name: string) =>
|
||||
name.endsWith('s') ? `${name}'` : `${name}'s`
|
||||
Reference in New Issue
Block a user