Files
kycnotme/web/src/pages/account/index.astro
2025-05-23 18:23:14 +00:00

975 lines
38 KiB
Plaintext

---
import { Icon } from 'astro-icon/components'
import { actions } from 'astro:actions'
import { sortBy } from 'lodash-es'
import BadgeSmall from '../../components/BadgeSmall.astro'
import Button from '../../components/Button.astro'
import MyPicture from '../../components/MyPicture.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
import Tooltip from '../../components/Tooltip.astro'
import UserBadge from '../../components/UserBadge.astro'
import { getKarmaTransactionActionInfo } from '../../constants/karmaTransactionActions'
import { karmaUnlocks, karmaUnlocksById } from '../../constants/karmaUnlocks'
import { SUPPORT_EMAIL } from '../../constants/project'
import { getServiceSuggestionStatusInfo } from '../../constants/serviceSuggestionStatus'
import { getServiceSuggestionTypeInfo } from '../../constants/serviceSuggestionType'
import { getServiceUserRoleInfo } from '../../constants/serviceUserRoles'
import { verificationStatusesByValue } from '../../constants/verificationStatus'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { cn } from '../../lib/cn'
import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks'
import { prisma } from '../../lib/prisma'
import { makeLoginUrl } from '../../lib/redirectUrls'
import { formatDateShort } from '../../lib/timeAgo'
const userId = Astro.locals.user?.id
if (!userId) {
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Login to view your account' }))
}
const updateResult = Astro.getActionResult(actions.account.update)
Astro.locals.banners.addIfSuccess(updateResult, 'Profile updated successfully')
const user = await Astro.locals.banners.try('user', async () => {
return makeUserWithKarmaUnlocks(
await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
displayName: true,
link: true,
picture: true,
spammer: true,
verified: true,
admin: true,
moderator: true,
verifiedLink: true,
totalKarma: true,
createdAt: true,
_count: {
select: {
comments: true,
commentVotes: true,
karmaTransactions: true,
},
},
karmaTransactions: {
select: {
id: true,
points: true,
action: true,
description: true,
createdAt: true,
grantedBy: {
select: {
name: true,
displayName: true,
picture: true,
},
},
comment: {
select: {
id: true,
content: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 5,
},
suggestions: {
select: {
id: true,
type: true,
status: true,
createdAt: true,
service: {
select: {
id: true,
name: true,
slug: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 5,
},
comments: {
select: {
id: true,
content: true,
createdAt: true,
upvotes: true,
status: true,
service: {
select: {
id: true,
name: true,
slug: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 5,
},
commentVotes: {
select: {
id: true,
downvote: true,
createdAt: true,
comment: {
select: {
id: true,
content: true,
service: {
select: {
id: true,
name: true,
slug: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
take: 5,
},
serviceAffiliations: {
select: {
role: true,
service: {
select: {
id: true,
name: true,
slug: true,
imageUrl: true,
verificationStatus: true,
},
},
},
orderBy: [{ role: 'asc' }, { service: { name: 'asc' } }],
},
},
})
)
})
if (!user) return Astro.rewrite('/404')
---
<BaseLayout
pageTitle={`${user.displayName ?? user.name} - Account`}
description="Manage your user profile"
ogImage={{
template: 'generic',
title: `${user.displayName ?? user.name} | Account`,
description: 'Manage your user profile',
icon: 'ri:user-3-line',
}}
widthClassName="max-w-screen-md"
className={{
main: 'space-y-6',
}}
breadcrumbs={[
{
name: 'Accounts',
url: '/account',
},
{
name: 'My account',
},
]}
>
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex flex-wrap items-center justify-center gap-4">
<div class="flex grow flex-wrap items-center justify-center gap-4">
{
user.picture ? (
<MyPicture
src={user.picture}
alt=""
class="ring-day-500/30 xs:size-14 size-12 rounded-full ring-2 sm:size-16"
width={64}
height={64}
/>
) : (
<div class="bg-day-500/10 ring-day-500/30 text-day-400 flex size-16 items-center justify-center rounded-full ring-2">
<Icon name="ri:user-3-line" class="size-8" />
</div>
)
}
<div class="grow">
<h1 class="font-title text-lg font-bold tracking-wider text-white">
{user.displayName ?? user.name}
</h1>
{user.displayName && <p class="text-day-200 font-title">{user.name}</p>}
{
(user.admin || user.verified || user.moderator) && (
<div class="mt-1 flex gap-2">
{user.admin && (
<span class="rounded-full border border-red-500/50 bg-red-500/20 px-2 py-0.5 text-xs text-red-400">
admin
</span>
)}
{user.verified && (
<span class="rounded-full border border-blue-500/50 bg-blue-500/20 px-2 py-0.5 text-xs text-blue-400">
verified
</span>
)}
{user.moderator && (
<span class="rounded-full border border-green-500/50 bg-green-500/20 px-2 py-0.5 text-xs text-green-400">
moderator
</span>
)}
</div>
)
}
</div>
</div>
<nav class="flex flex-wrap items-center justify-center gap-2">
<Tooltip
as="a"
href={`/u/${user.name}`}
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-1 rounded-md border p-2 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
text="View public profile"
>
<Icon name="ri:global-line" class="size-4" />
</Tooltip>
<Tooltip
as="a"
href="/account/logout"
data-astro-prefetch="tap"
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-1 rounded-md border p-2 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
text="Logout"
>
<Icon name="ri:logout-box-r-line" class="size-4" />
</Tooltip>
<Tooltip
as="a"
text="Edit Profile"
href="/account/edit"
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-1 rounded-md border p-2 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
>
<Icon name="ri:edit-line" class="size-4" />
</Tooltip>
</nav>
</header>
<div class="border-night-400 mt-6 border-t pt-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<h3 class="font-title text-day-200 mb-4 text-sm">Profile Information</h3>
<ul class="flex flex-col items-start gap-2">
<li class="flex items-start">
<span class="text-day-500 mt-0.5 mr-2"><Icon name="ri:user-3-line" class="size-4" /></span>
<div>
<p class="text-day-500 text-xs">Username</p>
<p class="text-day-300">{user.name}</p>
</div>
</li>
<Tooltip as="li" text="Unlock with more karma" class="inline-flex items-start">
<span class="text-day-500 mt-0.5 mr-2">
<Icon name="ri:user-smile-line" class="size-4" />
</span>
<div>
<p class="text-day-500 text-xs">
Display Name {
!user.karmaUnlocks.displayName && (
<Icon name="ri:lock-line" class="inline-block size-3 align-[-0.15em]" />
)
}
</p>
<p class="text-day-300">
{user.displayName ?? <span class="text-sm italic">Not set</span>}
</p>
</div>
</Tooltip>
<Tooltip as="li" text="Unlock with more karma" class="inline-flex items-start">
<span class="text-day-500 mt-0.5 mr-2">
<Icon name="ri:link" class="size-4" />
</span>
<div>
<p class="text-day-500 text-xs">
Website {
!user.karmaUnlocks.websiteLink && (
<Icon name="ri:lock-line" class="inline-block size-3 align-[-0.15em]" />
)
}
</p>
<p class="text-day-300">
{
user.link ? (
<a
href={user.link}
target="_blank"
rel="noopener noreferrer"
class="text-blue-400 hover:underline"
>
{user.link}
</a>
) : (
<span class="text-sm italic">Not set</span>
)
}
</p>
</div>
</Tooltip>
<li class="flex items-start">
<span class="text-day-500 mt-0.5 mr-2"><Icon name="ri:award-line" class="size-4" /></span>
<div>
<p class="text-day-500 text-xs">Karma</p>
<p class="text-day-300">{user.totalKarma.toLocaleString()}</p>
</div>
</li>
</ul>
</div>
<div id="account-status">
<h3 class="font-title text-day-200 mb-4 text-sm">Account Status</h3>
<ul class="space-y-3">
<li class="flex items-start">
<span class="text-day-500 mt-0.5 mr-2">
<Icon name="ri:shield-check-line" class="size-4" />
</span>
<div>
<p class="text-day-500 text-xs">Account Type</p>
<div class="mt-1 flex flex-wrap gap-2">
{
user.admin && (
<span class="rounded-full border border-red-500/50 bg-red-500/20 px-2 py-0.5 text-xs text-red-400">
Admin
</span>
)
}
{
user.verified && (
<span class="rounded-full border border-blue-500/50 bg-blue-500/20 px-2 py-0.5 text-xs text-blue-400">
Verified User
</span>
)
}
{
user.moderator && (
<span class="rounded-full border border-green-500/50 bg-green-500/20 px-2 py-0.5 text-xs text-green-400">
Moderator
</span>
)
}
{
!user.admin && !user.verified && !user.moderator && (
<span class="border-day-700/50 bg-day-700/20 text-day-400 rounded-full border px-2 py-0.5 text-xs">
Standard User
</span>
)
}
</div>
</div>
</li>
<li class="flex items-start">
<span class="text-day-500 mt-0.5 mr-2">
<Icon name="ri:spam-2-line" class="size-4" />
</span>
<div>
<p class="text-day-500 text-xs">Spam Status</p>
{
user.spammer ? (
<span class="rounded-full border border-red-500/50 bg-red-500/20 px-2 py-0.5 text-xs text-red-400">
Spammer
</span>
) : (
<span class="rounded-full border border-green-500/50 bg-green-500/20 px-2 py-0.5 text-xs text-green-400">
Not Flagged
</span>
)
}
</div>
</li>
<li class="flex items-start">
<span class="text-day-500 mt-0.5 mr-2"><Icon name="ri:calendar-line" class="size-4" /></span>
<div>
<p class="text-day-500 text-xs">Joined</p>
<p class="text-day-300">
{
formatDateShort(user.createdAt, {
prefix: false,
hourPrecision: true,
caseType: 'sentence',
})
}
</p>
</div>
</li>
{
user.verifiedLink && (
<li class="flex items-start">
<span class="text-day-500 mt-0.5 mr-2">
<Icon name="ri:check-double-line" class="size-4" />
</span>
<div>
<p class="text-day-500 text-xs">Verified as related to</p>
<a
href={user.verifiedLink}
target="_blank"
rel="noopener noreferrer"
class="text-blue-400 hover:underline"
>
{user.verifiedLink}
</a>
</div>
</li>
)
}
<li>
<Button
as="a"
href={`mailto:${SUPPORT_EMAIL}?subject=User verification request - ${user.displayName ?? user.name}&body=I would like to be verified as related to https://www.example.com`}
label="Request verification"
size="sm"
/>
</li>
</ul>
</div>
</div>
</div>
</section>
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex items-center justify-between">
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:building-4-line" class="mr-2 inline-block size-5" />
Service Affiliations
</h2>
<Button
as="a"
href={`mailto:${SUPPORT_EMAIL}?subject=Service Affiliation Verification Request - ${user.displayName ?? user.name}&body=I would like to be verified as related to the services ACME as Admin and XYZ as Team Member. Here is the proof...`}
label="Request"
size="md"
/>
</header>
{
user.serviceAffiliations.length > 0 ? (
<ul class="2xs:grid-cols-[repeat(auto-fit,minmax(200px,1fr))] grid gap-4">
{user.serviceAffiliations.map((affiliation) => {
const roleInfo = getServiceUserRoleInfo(affiliation.role)
const statusIcon = {
...verificationStatusesByValue,
APPROVED: undefined,
}[affiliation.service.verificationStatus]
return (
<li class="shrink-0">
<a
href={`/service/${affiliation.service.slug}`}
class="text-day-300 group flex min-w-32 items-center gap-2 text-sm"
>
<MyPicture
src={affiliation.service.imageUrl}
fallback="service"
alt={affiliation.service.name}
width={40}
height={40}
class="size-10 shrink-0 rounded-lg"
/>
<div class="flex min-w-0 flex-1 flex-col justify-center">
<div class="flex items-center gap-1.5">
<BadgeSmall color={roleInfo.color} text={roleInfo.label} icon={roleInfo.icon} />
<span class="text-day-500">of</span>
</div>
<div class="text-day-300 flex items-center gap-1 font-semibold">
<span class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap group-hover:underline">
{affiliation.service.name}
</span>
{statusIcon && (
<Tooltip text={statusIcon.label} position="right" class="-m-1 shrink-0">
<Icon
name={statusIcon.icon}
class={cn(
'inline-block size-6 shrink-0 rounded-lg p-1',
statusIcon.classNames.icon
)}
/>
</Tooltip>
)}
<Icon name="ri:external-link-line" class="size-4 shrink-0" />
</div>
</div>
</a>
</li>
)
})}
</ul>
) : (
<p class="text-day-400 mb-6">No service affiliations yet.</p>
)
}
</section>
<section
class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs"
id="karma-unlocks"
>
<header>
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:lock-unlock-line" class="mr-2 inline-block size-5" />
Karma Unlocks
</h2>
<div class="border-night-500 bg-night-800/70 mb-4 rounded-md border px-4 py-3">
<p class="text-day-300">
Earn karma to unlock features and privileges. <a href="/karma" class="text-day-200 hover:underline"
>Learn about karma</a
>
</p>
</div>
</header>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-3">
<input type="checkbox" id="positive-unlocks-toggle" class="peer sr-only md:hidden" checked />
<label
for="positive-unlocks-toggle"
class="flex cursor-pointer items-center justify-between border-b border-green-500/20 pb-2 md:cursor-default peer-checked:[&_[data-expand-arrow]]:rotate-180"
>
<h3 class="font-title text-day-200 text-sm">Positive unlocks</h3>
<Icon name="ri:arrow-down-s-line" class="text-day-400 size-5 md:hidden" data-expand-arrow />
</label>
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
{
sortBy(
karmaUnlocks.filter((unlock) => unlock.karma >= 0),
'karma'
).map((unlock) => (
<div
class={cn(
'flex items-center justify-between rounded-md border p-3',
user.karmaUnlocks[unlock.id]
? 'border-green-500/30 bg-green-500/10'
: 'border-night-500 bg-night-800'
)}
>
<div class="flex items-center">
<span class={cn('mr-3', user.karmaUnlocks[unlock.id] ? 'text-day-400' : 'text-day-500')}>
<Icon name={unlock.icon} class="size-5" />
</span>
<div>
<p
class={cn(
'font-medium',
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
)}
>
{unlock.name}
</p>
<p class="text-day-500 text-sm">{unlock.karma.toLocaleString()} karma</p>
</div>
</div>
<div>
{user.karmaUnlocks[unlock.id] ? (
<span class="bg-day-500/20 text-day-300 inline-flex items-center rounded-full px-2 py-1 text-xs">
<Icon name="ri:check-line" class="mr-1 size-3" /> Unlocked
</span>
) : (
<span class="bg-night-800 text-day-400 inline-flex items-center rounded-full px-2 py-1 text-xs">
<Icon name="ri:lock-line" class="mr-1 size-3" /> Locked
</span>
)}
</div>
</div>
))
}
</div>
</div>
<div class="space-y-3">
<input type="checkbox" id="negative-unlocks-toggle" class="peer sr-only md:hidden" />
<label
for="negative-unlocks-toggle"
class="flex cursor-pointer items-center justify-between border-b border-red-500/20 pb-2 md:cursor-default peer-checked:[&_[data-expand-arrow]]:rotate-180"
>
<h3 class="font-title text-sm text-red-400">Negative unlocks</h3>
<Icon name="ri:arrow-down-s-line" class="text-day-400 size-5 md:hidden" data-expand-arrow />
</label>
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
{
sortBy(
karmaUnlocks.filter((unlock) => unlock.karma < 0),
'karma'
)
.reverse()
.map((unlock) => (
<div
class={cn(
'flex items-center justify-between rounded-md border p-3',
user.karmaUnlocks[unlock.id]
? 'border-red-500/30 bg-red-500/10'
: 'border-night-500 bg-night-800'
)}
>
<div class="flex items-center">
<span class={cn('mr-3', user.karmaUnlocks[unlock.id] ? 'text-red-400' : 'text-day-500')}>
<Icon name={unlock.icon} class="size-5" />
</span>
<div>
<p
class={cn(
'font-medium',
user.karmaUnlocks[unlock.id] ? 'text-red-400' : 'text-day-400'
)}
>
{unlock.name}
</p>
<p class="text-day-500 text-sm">{unlock.karma.toLocaleString()} karma</p>
</div>
</div>
<div>
{user.karmaUnlocks[unlock.id] ? (
<span class="inline-flex items-center rounded-full bg-red-500/20 px-2 py-1 text-xs text-red-400">
<Icon name="ri:alert-line" class="mr-1 size-3" /> Active
</span>
) : (
<span class="bg-night-800 text-day-400 inline-flex items-center rounded-full px-2 py-1 text-xs">
<Icon name="ri:shield-check-line" class="mr-1 size-3" /> Avoided
</span>
)}
</div>
</div>
))
}
<p class="text-day-400 border-night-500/30 bg-night-800/70 mt-4 rounded-md border p-3 text-xs">
<Icon name="ri:information-line" class="inline-block size-4" />
Negative karma leads to restrictions. <br class="hidden sm:block" />Keep interactions positive to
avoid penalties.
</p>
</div>
</div>
</div>
</section>
<div class="space-y-6">
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex items-center justify-between">
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:chat-3-line" class="mr-2 inline-block size-5" />
Recent Comments
</h2>
<span class="text-day-500">{user._count.comments.toLocaleString()} comments</span>
</header>
{
user.comments.length === 0 ? (
<p class="text-day-400">No comments yet.</p>
) : (
<div class="overflow-x-auto">
<table class="divide-night-400/20 w-full min-w-full divide-y">
<thead>
<tr>
<th class="text-day-400 px-4 py-3 text-left text-xs">Service</th>
<th class="text-day-400 px-4 py-3 text-left text-xs">Comment</th>
<th class="text-day-400 px-4 py-3 text-center text-xs">Status</th>
<th class="text-day-400 px-4 py-3 text-center text-xs">Upvotes</th>
<th class="text-day-400 px-4 py-3 text-right text-xs">Date</th>
</tr>
</thead>
<tbody class="divide-night-400/10 divide-y">
{user.comments.map((comment) => (
<tr class="hover:bg-night-500/5">
<td class="text-day-300 px-4 py-3 text-xs whitespace-nowrap">
<a href={`/service/${comment.service.slug}`} class="text-blue-400 hover:underline">
{comment.service.name}
</a>
</td>
<td class="text-day-300 px-4 py-3">
<p class="line-clamp-1">{comment.content}</p>
</td>
<td class="px-4 py-3 text-center">
<span class="border-day-700/20 bg-night-800/30 text-day-300 inline-flex rounded-full border px-2 py-0.5 text-xs">
{comment.status}
</span>
</td>
<td class="text-day-300 px-4 py-3 text-center text-xs">
<span class="inline-flex items-center">
<Icon name="ri:thumb-up-line" class="mr-1 size-4" /> {comment.upvotes}
</span>
</td>
<td class="text-day-400 px-4 py-3 text-right text-xs whitespace-nowrap">
<TimeFormatted
date={comment.createdAt}
prefix={false}
hourPrecision
caseType="sentence"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
</section>
{
user.karmaUnlocks.voteComments || user._count.commentVotes ? (
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex items-center justify-between">
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:thumb-up-line" class="mr-2 inline-block size-5" />
Recent Votes
</h2>
<span class="text-day-500">{user._count.commentVotes.toLocaleString()} votes</span>
</header>
{user.commentVotes.length === 0 ? (
<p class="text-day-400">No votes yet.</p>
) : (
<div class="overflow-x-auto">
<table class="divide-night-400/20 w-full min-w-full divide-y">
<thead>
<tr>
<th class="text-day-400 px-4 py-3 text-left text-xs">Service</th>
<th class="text-day-400 px-4 py-3 text-left text-xs">Comment</th>
<th class="text-day-400 px-4 py-3 text-center text-xs">Vote</th>
<th class="text-day-400 px-4 py-3 text-right text-xs">Date</th>
</tr>
</thead>
<tbody class="divide-night-400/10 divide-y">
{user.commentVotes.map((vote) => (
<tr class="hover:bg-night-500/5">
<td class="text-day-300 px-4 py-3 text-xs whitespace-nowrap">
<a
href={`/service/${vote.comment.service.slug}`}
class="text-blue-400 hover:underline"
>
{vote.comment.service.name}
</a>
</td>
<td class="text-day-300 px-4 py-3">
<p class="line-clamp-1">{vote.comment.content}</p>
</td>
<td class="px-4 py-3 text-center">
{vote.downvote ? (
<span class="inline-flex items-center text-red-400">
<Icon name="ri:thumb-down-fill" class="size-4" />
</span>
) : (
<span class="inline-flex items-center text-green-400">
<Icon name="ri:thumb-up-fill" class="size-4" />
</span>
)}
</td>
<td class="text-day-400 px-4 py-3 text-right text-xs whitespace-nowrap">
<TimeFormatted
date={vote.createdAt}
prefix={false}
hourPrecision
caseType="sentence"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
) : (
<section class="border-night-400 bg-night-400/5 flex flex-wrap items-center justify-between gap-2 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<h2 class="font-title text-day-200 grow-9999 text-xl font-bold">
<Icon name="ri:thumb-up-line" class="mr-2 inline-block size-5" />
Recent Votes
</h2>
<Tooltip
text={`Get ${(karmaUnlocksById.voteComments.karma - user.totalKarma).toLocaleString()} more karma to unlock`}
class="text-day-500 inline-flex grow items-center justify-center gap-1"
>
<Icon name="ri:lock-line" class="inline-block size-5" />
Locked
</Tooltip>
</section>
)
}
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex items-center justify-between">
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:lightbulb-line" class="mr-2 inline-block size-5" />
Recent Suggestions
</h2>
<a
href="/service-suggestion"
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
>
View all
</a>
</header>
{
user.suggestions.length === 0 ? (
<p class="text-day-400">No suggestions yet.</p>
) : (
<div class="overflow-x-auto">
<table class="divide-night-400/20 w-full min-w-full divide-y">
<thead>
<tr>
<th class="text-day-400 px-4 py-3 text-left text-xs">Service</th>
<th class="text-day-400 px-4 py-3 text-left text-xs">Type</th>
<th class="text-day-400 px-4 py-3 text-center text-xs">Status</th>
<th class="text-day-400 px-4 py-3 text-right text-xs">Date</th>
</tr>
</thead>
<tbody class="divide-night-400/10 divide-y">
{user.suggestions.map((suggestion) => {
const typeInfo = getServiceSuggestionTypeInfo(suggestion.type)
const statusInfo = getServiceSuggestionStatusInfo(suggestion.status)
return (
<tr class="hover:bg-night-500/5">
<td class="text-day-300 px-4 py-3 text-xs whitespace-nowrap">
<a
href={`/service-suggestion/${suggestion.id}`}
class="text-blue-400 hover:underline"
>
{suggestion.service.name}
</a>
</td>
<td class="text-day-300 px-4 py-3 text-xs whitespace-nowrap">
<span class="inline-flex items-center">
<Icon name={typeInfo.icon} class="mr-1 size-4" />
{typeInfo.label}
</span>
</td>
<td class="px-4 py-3 text-center">
<span
class={cn(
'border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs',
statusInfo.iconClass
)}
>
<Icon name={statusInfo.icon} class="mr-1 size-3" />
{statusInfo.label}
</span>
</td>
<td class="text-day-400 px-4 py-3 text-right text-xs whitespace-nowrap">
<TimeFormatted
date={suggestion.createdAt}
prefix={false}
hourPrecision
caseType="sentence"
/>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
</section>
<section
class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs"
id="karma-transactions"
>
<header class="flex items-center justify-between">
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:exchange-line" class="mr-2 inline-block size-5" />
Recent Karma Transactions
</h2>
<span class="text-day-500">{user.totalKarma.toLocaleString()} karma</span>
</header>
{
user.karmaTransactions.length === 0 ? (
<p class="text-day-400">No karma transactions yet.</p>
) : (
<div class="overflow-x-auto">
<table class="divide-night-400/20 w-full min-w-full divide-y">
<thead>
<tr>
<th class="text-day-400 px-4 py-3 text-left text-xs">Action</th>
<th class="text-day-400 px-4 py-3 text-left text-xs">Description</th>
<th class="text-day-400 px-4 py-3 text-right text-xs">Points</th>
<th class="text-day-400 px-4 py-3 text-right text-xs">Date</th>
</tr>
</thead>
<tbody class="divide-night-400/10 divide-y">
{user.karmaTransactions.map((transaction) => {
const actionInfo = getKarmaTransactionActionInfo(transaction.action)
return (
<tr class="hover:bg-night-500/5">
<td class="text-day-300 px-4 py-3 text-xs whitespace-nowrap">
<span class="inline-flex items-center gap-1">
<Icon name={actionInfo.icon} class="size-4" />
{actionInfo.label}
{transaction.action === 'MANUAL_ADJUSTMENT' && transaction.grantedBy && (
<>
<span class="text-day-500">from</span>
<UserBadge user={transaction.grantedBy} size="sm" class="text-day-500" />
</>
)}
</span>
</td>
<td class="text-day-300 px-4 py-3">{transaction.description}</td>
<td
class={cn(
'px-4 py-3 text-right text-xs whitespace-nowrap',
transaction.points >= 0 ? 'text-green-400' : 'text-red-400'
)}
>
{transaction.points >= 0 && '+'}
{transaction.points}
</td>
<td class="text-day-400 px-4 py-3 text-right text-xs whitespace-nowrap">
<TimeFormatted
date={transaction.createdAt}
prefix={false}
hourPrecision
caseType="sentence"
/>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
</section>
</div>
<nav class="mt-6 flex items-center justify-center gap-4">
<a
href="/account/logout"
data-astro-prefetch="tap"
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
>
<Icon name="ri:logout-box-r-line" class="size-4" /> Logout
</a>
<a
href={`mailto:${SUPPORT_EMAIL}`}
class="inline-flex items-center gap-1 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-1.5 text-sm text-red-400 shadow-xs transition-colors duration-200 hover:bg-red-500/20 focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
>
<Icon name="ri:delete-bin-line" class="size-4" /> Delete account
</a>
</nav>
</BaseLayout>