Release 2025-05-21-MXjT
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "NotificationType" ADD VALUE 'KARMA_CHANGE';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Notification" ADD COLUMN "aboutKarmaTransactionId" INTEGER;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "NotificationPreferences" ADD COLUMN "karmaNotificationThreshold" INTEGER NOT NULL DEFAULT 10;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutKarmaTransactionId_fkey" FOREIGN KEY ("aboutKarmaTransactionId") REFERENCES "KarmaTransaction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -135,6 +135,7 @@ enum NotificationType {
|
||||
SUGGESTION_MESSAGE
|
||||
SUGGESTION_STATUS_CHANGE
|
||||
// KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
||||
KARMA_CHANGE
|
||||
/// Marked as spammer, promoted to admin, etc.
|
||||
ACCOUNT_STATUS_CHANGE
|
||||
EVENT_CREATED
|
||||
@@ -207,6 +208,8 @@ model Notification {
|
||||
aboutCommentStatusChange CommentStatusChange?
|
||||
aboutServiceVerificationStatusChange ServiceVerificationStatusChange?
|
||||
aboutSuggestionStatusChange ServiceSuggestionStatusChange?
|
||||
aboutKarmaTransaction KarmaTransaction? @relation(fields: [aboutKarmaTransactionId], references: [id])
|
||||
aboutKarmaTransactionId Int?
|
||||
|
||||
@@index([userId])
|
||||
@@index([read])
|
||||
@@ -229,6 +232,7 @@ model NotificationPreferences {
|
||||
enableOnMyCommentStatusChange Boolean @default(true)
|
||||
enableAutowatchMyComments Boolean @default(true)
|
||||
enableNotifyPendingRepliesOnWatch Boolean @default(false)
|
||||
karmaNotificationThreshold Int @default(10)
|
||||
|
||||
onEventCreatedForServices Service[] @relation("onEventCreatedForServices")
|
||||
onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices")
|
||||
@@ -522,6 +526,7 @@ model KarmaTransaction {
|
||||
createdAt DateTime @default(now())
|
||||
grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull)
|
||||
grantedByUserId Int?
|
||||
Notification Notification[]
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([userId])
|
||||
|
||||
29
web/prisma/triggers/11_notifications_karma.sql
Normal file
29
web/prisma/triggers/11_notifications_karma.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
CREATE OR REPLACE FUNCTION trigger_karma_notifications()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Only create notification if the user has enabled karma notifications
|
||||
-- and the karma change exceeds their threshold
|
||||
INSERT INTO "Notification" ("userId", "type", "aboutKarmaTransactionId")
|
||||
SELECT NEW."userId", 'KARMA_CHANGE', NEW.id
|
||||
FROM "NotificationPreferences" np
|
||||
WHERE np."userId" = NEW."userId"
|
||||
AND ABS(NEW.points) >= COALESCE(np."karmaNotificationThreshold", 10)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "Notification" n
|
||||
WHERE n."userId" = NEW."userId"
|
||||
AND n."type" = 'KARMA_CHANGE'
|
||||
AND n."aboutKarmaTransactionId" = NEW.id
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Drop the trigger if it exists to ensure a clean setup
|
||||
DROP TRIGGER IF EXISTS karma_notifications_trigger ON "KarmaTransaction";
|
||||
|
||||
-- Create the trigger to fire after inserts
|
||||
CREATE TRIGGER karma_notifications_trigger
|
||||
AFTER INSERT ON "KarmaTransaction"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_karma_notifications();
|
||||
@@ -31,6 +31,7 @@ export const notificationActions = {
|
||||
enableOnMyCommentStatusChange: z.coerce.boolean().optional(),
|
||||
enableAutowatchMyComments: z.coerce.boolean().optional(),
|
||||
enableNotifyPendingRepliesOnWatch: z.coerce.boolean().optional(),
|
||||
karmaNotificationThreshold: z.coerce.number().int().min(1).optional(),
|
||||
}),
|
||||
handler: async (input, context) => {
|
||||
await prisma.notificationPreferences.upsert({
|
||||
@@ -39,12 +40,14 @@ export const notificationActions = {
|
||||
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
|
||||
enableAutowatchMyComments: input.enableAutowatchMyComments,
|
||||
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
|
||||
karmaNotificationThreshold: input.karmaNotificationThreshold,
|
||||
},
|
||||
create: {
|
||||
userId: context.locals.user.id,
|
||||
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
|
||||
enableAutowatchMyComments: input.enableAutowatchMyComments,
|
||||
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
|
||||
karmaNotificationThreshold: input.karmaNotificationThreshold,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@ const Tag = announcement.link ? 'a' : 'div'
|
||||
<Icon name={typeInfo.icon} class={cn('size-5 flex-shrink-0')} />
|
||||
<span
|
||||
class={cn(
|
||||
'font-title line-clamp-3 bg-[linear-gradient(90deg,var(--gradient-edge,#FFEBF9)_0%,var(--gradient-center,#8a56cc)_50%,var(--gradient-edge,#FFEBF9)_100%)] bg-clip-text text-sm leading-tight text-pretty text-transparent [&_a]:underline',
|
||||
'font-title animate-text-gradient line-clamp-3 bg-[linear-gradient(90deg,var(--gradient-edge,#FFEBF9)_0%,var(--gradient-center,#8a56cc)_50%,var(--gradient-edge,#FFEBF9)_100%)] bg-size-[200%] bg-clip-text text-sm leading-tight text-pretty text-transparent [&_a]:underline',
|
||||
typeInfo.classNames.content
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -25,12 +25,12 @@ export const {
|
||||
(value): AnnouncementTypeInfo<typeof value> => ({
|
||||
value,
|
||||
label: value ? transformCase(value.replaceAll('_', ' '), 'title') : String(value),
|
||||
icon: 'ri:information-fill',
|
||||
icon: 'ri:question-line',
|
||||
classNames: {
|
||||
container: 'bg-blue-950',
|
||||
bg: 'from-blue-400 to-blue-700',
|
||||
content: '[--gradient-edge:var(--color-blue-100)] [--gradient-center:var(--color-blue-200)]',
|
||||
icon: 'text-blue-400/80',
|
||||
container: 'bg-cyan-950',
|
||||
bg: 'from-cyan-400 to-cyan-700',
|
||||
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]',
|
||||
icon: 'text-cyan-300/80',
|
||||
badge: 'bg-blue-900/30 text-blue-400',
|
||||
},
|
||||
}),
|
||||
@@ -38,12 +38,12 @@ export const {
|
||||
{
|
||||
value: 'INFO',
|
||||
label: 'Info',
|
||||
icon: 'ri:information-fill',
|
||||
icon: 'ri:information-line',
|
||||
classNames: {
|
||||
container: 'bg-green-950',
|
||||
bg: 'from-green-400 to-green-700',
|
||||
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-lime-200)]',
|
||||
icon: 'text-green-400/80',
|
||||
container: 'bg-cyan-950',
|
||||
bg: 'from-cyan-400 to-cyan-700',
|
||||
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]',
|
||||
icon: 'text-cyan-300/80',
|
||||
badge: 'bg-blue-900/30 text-blue-400',
|
||||
},
|
||||
},
|
||||
@@ -54,7 +54,7 @@ export const {
|
||||
classNames: {
|
||||
container: 'bg-yellow-950',
|
||||
bg: 'from-yellow-400 to-yellow-700',
|
||||
content: '[--gradient-edge:var(--color-yellow-100)] [--gradient-center:var(--color-amber-200)]',
|
||||
content: '[--gradient-edge:var(--color-lime-100)] [--gradient-center:var(--color-yellow-400)]',
|
||||
icon: 'text-yellow-400/80',
|
||||
badge: 'bg-yellow-900/30 text-yellow-400',
|
||||
},
|
||||
@@ -66,7 +66,7 @@ export const {
|
||||
classNames: {
|
||||
container: 'bg-red-950',
|
||||
bg: 'from-red-400 to-red-700',
|
||||
content: '[--gradient-edge:var(--color-red-100)] [--gradient-center:var(--color-rose-200)]',
|
||||
content: '[--gradient-edge:var(--color-red-100)] [--gradient-center:var(--color-rose-400)]',
|
||||
icon: 'text-red-400/80',
|
||||
badge: 'bg-red-900/30 text-red-400',
|
||||
},
|
||||
|
||||
@@ -46,11 +46,11 @@ export const {
|
||||
icon: 'ri:lightbulb-line',
|
||||
},
|
||||
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
||||
// {
|
||||
// id: 'KARMA_UNLOCK',
|
||||
// label: 'Karma unlock',
|
||||
// icon: 'ri:award-line',
|
||||
// },
|
||||
{
|
||||
id: 'KARMA_CHANGE',
|
||||
label: 'Karma recieved',
|
||||
icon: 'ri:award-line',
|
||||
},
|
||||
{
|
||||
id: 'ACCOUNT_STATUS_CHANGE',
|
||||
label: 'Change in account status',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { accountStatusChangesById } from '../constants/accountStatusChange'
|
||||
import { commentStatusChangesById } from '../constants/commentStatusChange'
|
||||
import { eventTypesById } from '../constants/eventTypes'
|
||||
import { getKarmaTransactionActionInfo } from '../constants/karmaTransactionActions'
|
||||
import { serviceVerificationStatusChangesById } from '../constants/serviceStatusChange'
|
||||
import { serviceSuggestionStatusChangesById } from '../constants/suggestionStatusChange'
|
||||
|
||||
@@ -16,6 +17,12 @@ export function makeNotificationTitle(
|
||||
aboutCommentStatusChange: true
|
||||
aboutServiceVerificationStatusChange: true
|
||||
aboutSuggestionStatusChange: true
|
||||
aboutKarmaTransaction: {
|
||||
select: {
|
||||
points: true
|
||||
action: true
|
||||
}
|
||||
}
|
||||
aboutComment: {
|
||||
select: {
|
||||
author: { select: { id: true } }
|
||||
@@ -137,6 +144,13 @@ export function makeNotificationTitle(
|
||||
// case 'KARMA_UNLOCK': {
|
||||
// return 'New karma level unlocked'
|
||||
// }
|
||||
case 'KARMA_CHANGE': {
|
||||
if (!notification.aboutKarmaTransaction) return 'Your karma has changed'
|
||||
const { points, action } = notification.aboutKarmaTransaction
|
||||
const sign = points > 0 ? '+' : ''
|
||||
const karmaInfo = getKarmaTransactionActionInfo(action)
|
||||
return `${sign}${points.toLocaleString()} karma for ${karmaInfo.label}`
|
||||
}
|
||||
case 'ACCOUNT_STATUS_CHANGE': {
|
||||
if (!notification.aboutAccountStatusChange) return 'Your account status has been updated'
|
||||
const accountStatusChange = accountStatusChangesById[notification.aboutAccountStatusChange]
|
||||
@@ -165,6 +179,11 @@ export function makeNotificationContent(
|
||||
notification: Prisma.NotificationGetPayload<{
|
||||
select: {
|
||||
type: true
|
||||
aboutKarmaTransaction: {
|
||||
select: {
|
||||
description: true
|
||||
}
|
||||
}
|
||||
aboutComment: {
|
||||
select: {
|
||||
content: true
|
||||
@@ -187,6 +206,10 @@ export function makeNotificationContent(
|
||||
switch (notification.type) {
|
||||
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
||||
// case 'KARMA_UNLOCK':
|
||||
case 'KARMA_CHANGE': {
|
||||
if (!notification.aboutKarmaTransaction) return null
|
||||
return notification.aboutKarmaTransaction.description
|
||||
}
|
||||
case 'SUGGESTION_STATUS_CHANGE':
|
||||
case 'ACCOUNT_STATUS_CHANGE':
|
||||
case 'SERVICE_VERIFICATION_STATUS_CHANGE': {
|
||||
@@ -280,6 +303,9 @@ export function makeNotificationLink(
|
||||
// case 'KARMA_UNLOCK': {
|
||||
// return `${origin}/account#karma-unlocks`
|
||||
// }
|
||||
case 'KARMA_CHANGE': {
|
||||
return `${origin}/account#karma-transactions`
|
||||
}
|
||||
case 'ACCOUNT_STATUS_CHANGE': {
|
||||
return `${origin}/account#account-status`
|
||||
}
|
||||
|
||||
@@ -534,77 +534,38 @@ if (!user) return Astro.rewrite('/404')
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-title border-day-700/20 text-day-200 border-b pb-2 text-sm">Positive unlocks</h3>
|
||||
<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>
|
||||
|
||||
{
|
||||
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 class="space-y-3">
|
||||
<h3 class="font-title border-b border-red-500/20 pb-2 text-sm text-red-400">Negative unlocks</h3>
|
||||
|
||||
{
|
||||
sortBy(
|
||||
karmaUnlocks.filter((unlock) => unlock.karma < 0),
|
||||
'karma'
|
||||
)
|
||||
.reverse()
|
||||
.map((unlock) => (
|
||||
<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-red-500/30 bg-red-500/10'
|
||||
? '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-red-400' : 'text-day-500')}>
|
||||
<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-red-400' : 'text-day-400'
|
||||
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
|
||||
)}
|
||||
>
|
||||
{unlock.name}
|
||||
@@ -614,24 +575,85 @@ if (!user) return Astro.rewrite('/404')
|
||||
</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 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:shield-check-line" class="mr-1 size-3" /> Avoided
|
||||
<Icon name="ri:lock-line" class="mr-1 size-3" /> Locked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
}
|
||||
</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 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>
|
||||
@@ -858,7 +880,10 @@ if (!user) return Astro.rewrite('/404')
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
|
||||
<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" />
|
||||
|
||||
@@ -124,6 +124,13 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast
|
||||
verificationStatus: true,
|
||||
},
|
||||
},
|
||||
aboutKarmaTransaction: {
|
||||
select: {
|
||||
points: true,
|
||||
action: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
@@ -136,6 +143,7 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast
|
||||
enableOnMyCommentStatusChange: true,
|
||||
enableAutowatchMyComments: true,
|
||||
enableNotifyPendingRepliesOnWatch: true,
|
||||
karmaNotificationThreshold: true,
|
||||
}),
|
||||
null,
|
||||
],
|
||||
@@ -326,6 +334,23 @@ const notifications = dbNotifications.map((notification) => ({
|
||||
</label>
|
||||
))}
|
||||
|
||||
<div class="mt-4 flex items-center justify-between rounded-md p-2 transition-colors duration-200 hover:bg-zinc-800">
|
||||
<span class="flex items-center text-zinc-300">
|
||||
<Icon name="ri:award-line" class="mr-2 size-5 text-zinc-400" />
|
||||
Notify me when my karma changes by at least
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
name="karmaNotificationThreshold"
|
||||
value={notificationPreferences.karmaNotificationThreshold}
|
||||
min="1"
|
||||
class="w-20 rounded border border-zinc-700 bg-zinc-800 px-2 py-1 text-zinc-200 focus:border-blue-600 focus:ring-1 focus:ring-blue-600 focus:outline-none"
|
||||
/>
|
||||
<span class="text-zinc-400">points</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button type="submit" label="Save" icon="ri:save-line" color="success" />
|
||||
</div>
|
||||
|
||||
@@ -632,77 +632,38 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="space-y-3">
|
||||
<h3 class="font-title border-day-700/20 text-day-200 border-b pb-2 text-sm">Positive unlocks</h3>
|
||||
<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>
|
||||
|
||||
{
|
||||
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 class="space-y-3">
|
||||
<h3 class="font-title border-b border-red-500/20 pb-2 text-sm text-red-400">Negative unlocks</h3>
|
||||
|
||||
{
|
||||
sortBy(
|
||||
karmaUnlocks.filter((unlock) => unlock.karma < 0),
|
||||
'karma'
|
||||
)
|
||||
.reverse()
|
||||
.map((unlock) => (
|
||||
<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-red-500/30 bg-red-500/10'
|
||||
? '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-red-400' : 'text-day-500')}>
|
||||
<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-red-400' : 'text-day-400'
|
||||
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
|
||||
)}
|
||||
>
|
||||
{unlock.name}
|
||||
@@ -712,24 +673,85 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
||||
</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 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:shield-check-line" class="mr-1 size-3" /> Avoided
|
||||
<Icon name="ri:lock-line" class="mr-1 size-3" /> Locked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
}
|
||||
</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 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>
|
||||
|
||||
@@ -105,6 +105,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--animate-text-gradient: text-gradient 4s linear 0s infinite normal forwards running;
|
||||
|
||||
@keyframes text-gradient {
|
||||
to {
|
||||
background-position: -200%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--ease-in-sine: cubic-bezier(0.12, 0, 0.39, 0);
|
||||
--ease-out-sine: cubic-bezier(0.61, 1, 0.88, 1);
|
||||
|
||||
Reference in New Issue
Block a user