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_MESSAGE
|
||||||
SUGGESTION_STATUS_CHANGE
|
SUGGESTION_STATUS_CHANGE
|
||||||
// KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
// 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.
|
/// Marked as spammer, promoted to admin, etc.
|
||||||
ACCOUNT_STATUS_CHANGE
|
ACCOUNT_STATUS_CHANGE
|
||||||
EVENT_CREATED
|
EVENT_CREATED
|
||||||
@@ -207,6 +208,8 @@ model Notification {
|
|||||||
aboutCommentStatusChange CommentStatusChange?
|
aboutCommentStatusChange CommentStatusChange?
|
||||||
aboutServiceVerificationStatusChange ServiceVerificationStatusChange?
|
aboutServiceVerificationStatusChange ServiceVerificationStatusChange?
|
||||||
aboutSuggestionStatusChange ServiceSuggestionStatusChange?
|
aboutSuggestionStatusChange ServiceSuggestionStatusChange?
|
||||||
|
aboutKarmaTransaction KarmaTransaction? @relation(fields: [aboutKarmaTransactionId], references: [id])
|
||||||
|
aboutKarmaTransactionId Int?
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([read])
|
@@index([read])
|
||||||
@@ -229,6 +232,7 @@ model NotificationPreferences {
|
|||||||
enableOnMyCommentStatusChange Boolean @default(true)
|
enableOnMyCommentStatusChange Boolean @default(true)
|
||||||
enableAutowatchMyComments Boolean @default(true)
|
enableAutowatchMyComments Boolean @default(true)
|
||||||
enableNotifyPendingRepliesOnWatch Boolean @default(false)
|
enableNotifyPendingRepliesOnWatch Boolean @default(false)
|
||||||
|
karmaNotificationThreshold Int @default(10)
|
||||||
|
|
||||||
onEventCreatedForServices Service[] @relation("onEventCreatedForServices")
|
onEventCreatedForServices Service[] @relation("onEventCreatedForServices")
|
||||||
onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices")
|
onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices")
|
||||||
@@ -522,6 +526,7 @@ model KarmaTransaction {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull)
|
grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull)
|
||||||
grantedByUserId Int?
|
grantedByUserId Int?
|
||||||
|
Notification Notification[]
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([userId])
|
@@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(),
|
enableOnMyCommentStatusChange: z.coerce.boolean().optional(),
|
||||||
enableAutowatchMyComments: z.coerce.boolean().optional(),
|
enableAutowatchMyComments: z.coerce.boolean().optional(),
|
||||||
enableNotifyPendingRepliesOnWatch: z.coerce.boolean().optional(),
|
enableNotifyPendingRepliesOnWatch: z.coerce.boolean().optional(),
|
||||||
|
karmaNotificationThreshold: z.coerce.number().int().min(1).optional(),
|
||||||
}),
|
}),
|
||||||
handler: async (input, context) => {
|
handler: async (input, context) => {
|
||||||
await prisma.notificationPreferences.upsert({
|
await prisma.notificationPreferences.upsert({
|
||||||
@@ -39,12 +40,14 @@ export const notificationActions = {
|
|||||||
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
|
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
|
||||||
enableAutowatchMyComments: input.enableAutowatchMyComments,
|
enableAutowatchMyComments: input.enableAutowatchMyComments,
|
||||||
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
|
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
|
||||||
|
karmaNotificationThreshold: input.karmaNotificationThreshold,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: context.locals.user.id,
|
userId: context.locals.user.id,
|
||||||
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
|
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
|
||||||
enableAutowatchMyComments: input.enableAutowatchMyComments,
|
enableAutowatchMyComments: input.enableAutowatchMyComments,
|
||||||
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
|
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')} />
|
<Icon name={typeInfo.icon} class={cn('size-5 flex-shrink-0')} />
|
||||||
<span
|
<span
|
||||||
class={cn(
|
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
|
typeInfo.classNames.content
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ export const {
|
|||||||
(value): AnnouncementTypeInfo<typeof value> => ({
|
(value): AnnouncementTypeInfo<typeof value> => ({
|
||||||
value,
|
value,
|
||||||
label: value ? transformCase(value.replaceAll('_', ' '), 'title') : String(value),
|
label: value ? transformCase(value.replaceAll('_', ' '), 'title') : String(value),
|
||||||
icon: 'ri:information-fill',
|
icon: 'ri:question-line',
|
||||||
classNames: {
|
classNames: {
|
||||||
container: 'bg-blue-950',
|
container: 'bg-cyan-950',
|
||||||
bg: 'from-blue-400 to-blue-700',
|
bg: 'from-cyan-400 to-cyan-700',
|
||||||
content: '[--gradient-edge:var(--color-blue-100)] [--gradient-center:var(--color-blue-200)]',
|
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]',
|
||||||
icon: 'text-blue-400/80',
|
icon: 'text-cyan-300/80',
|
||||||
badge: 'bg-blue-900/30 text-blue-400',
|
badge: 'bg-blue-900/30 text-blue-400',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -38,12 +38,12 @@ export const {
|
|||||||
{
|
{
|
||||||
value: 'INFO',
|
value: 'INFO',
|
||||||
label: 'Info',
|
label: 'Info',
|
||||||
icon: 'ri:information-fill',
|
icon: 'ri:information-line',
|
||||||
classNames: {
|
classNames: {
|
||||||
container: 'bg-green-950',
|
container: 'bg-cyan-950',
|
||||||
bg: 'from-green-400 to-green-700',
|
bg: 'from-cyan-400 to-cyan-700',
|
||||||
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-lime-200)]',
|
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]',
|
||||||
icon: 'text-green-400/80',
|
icon: 'text-cyan-300/80',
|
||||||
badge: 'bg-blue-900/30 text-blue-400',
|
badge: 'bg-blue-900/30 text-blue-400',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -54,7 +54,7 @@ export const {
|
|||||||
classNames: {
|
classNames: {
|
||||||
container: 'bg-yellow-950',
|
container: 'bg-yellow-950',
|
||||||
bg: 'from-yellow-400 to-yellow-700',
|
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',
|
icon: 'text-yellow-400/80',
|
||||||
badge: 'bg-yellow-900/30 text-yellow-400',
|
badge: 'bg-yellow-900/30 text-yellow-400',
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ export const {
|
|||||||
classNames: {
|
classNames: {
|
||||||
container: 'bg-red-950',
|
container: 'bg-red-950',
|
||||||
bg: 'from-red-400 to-red-700',
|
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',
|
icon: 'text-red-400/80',
|
||||||
badge: 'bg-red-900/30 text-red-400',
|
badge: 'bg-red-900/30 text-red-400',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ export const {
|
|||||||
icon: 'ri:lightbulb-line',
|
icon: 'ri:lightbulb-line',
|
||||||
},
|
},
|
||||||
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
||||||
// {
|
{
|
||||||
// id: 'KARMA_UNLOCK',
|
id: 'KARMA_CHANGE',
|
||||||
// label: 'Karma unlock',
|
label: 'Karma recieved',
|
||||||
// icon: 'ri:award-line',
|
icon: 'ri:award-line',
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
id: 'ACCOUNT_STATUS_CHANGE',
|
id: 'ACCOUNT_STATUS_CHANGE',
|
||||||
label: 'Change in account status',
|
label: 'Change in account status',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { accountStatusChangesById } from '../constants/accountStatusChange'
|
import { accountStatusChangesById } from '../constants/accountStatusChange'
|
||||||
import { commentStatusChangesById } from '../constants/commentStatusChange'
|
import { commentStatusChangesById } from '../constants/commentStatusChange'
|
||||||
import { eventTypesById } from '../constants/eventTypes'
|
import { eventTypesById } from '../constants/eventTypes'
|
||||||
|
import { getKarmaTransactionActionInfo } from '../constants/karmaTransactionActions'
|
||||||
import { serviceVerificationStatusChangesById } from '../constants/serviceStatusChange'
|
import { serviceVerificationStatusChangesById } from '../constants/serviceStatusChange'
|
||||||
import { serviceSuggestionStatusChangesById } from '../constants/suggestionStatusChange'
|
import { serviceSuggestionStatusChangesById } from '../constants/suggestionStatusChange'
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ export function makeNotificationTitle(
|
|||||||
aboutCommentStatusChange: true
|
aboutCommentStatusChange: true
|
||||||
aboutServiceVerificationStatusChange: true
|
aboutServiceVerificationStatusChange: true
|
||||||
aboutSuggestionStatusChange: true
|
aboutSuggestionStatusChange: true
|
||||||
|
aboutKarmaTransaction: {
|
||||||
|
select: {
|
||||||
|
points: true
|
||||||
|
action: true
|
||||||
|
}
|
||||||
|
}
|
||||||
aboutComment: {
|
aboutComment: {
|
||||||
select: {
|
select: {
|
||||||
author: { select: { id: true } }
|
author: { select: { id: true } }
|
||||||
@@ -137,6 +144,13 @@ export function makeNotificationTitle(
|
|||||||
// case 'KARMA_UNLOCK': {
|
// case 'KARMA_UNLOCK': {
|
||||||
// return 'New karma level unlocked'
|
// 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': {
|
case 'ACCOUNT_STATUS_CHANGE': {
|
||||||
if (!notification.aboutAccountStatusChange) return 'Your account status has been updated'
|
if (!notification.aboutAccountStatusChange) return 'Your account status has been updated'
|
||||||
const accountStatusChange = accountStatusChangesById[notification.aboutAccountStatusChange]
|
const accountStatusChange = accountStatusChangesById[notification.aboutAccountStatusChange]
|
||||||
@@ -165,6 +179,11 @@ export function makeNotificationContent(
|
|||||||
notification: Prisma.NotificationGetPayload<{
|
notification: Prisma.NotificationGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
type: true
|
type: true
|
||||||
|
aboutKarmaTransaction: {
|
||||||
|
select: {
|
||||||
|
description: true
|
||||||
|
}
|
||||||
|
}
|
||||||
aboutComment: {
|
aboutComment: {
|
||||||
select: {
|
select: {
|
||||||
content: true
|
content: true
|
||||||
@@ -187,6 +206,10 @@ export function makeNotificationContent(
|
|||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
|
||||||
// case 'KARMA_UNLOCK':
|
// case 'KARMA_UNLOCK':
|
||||||
|
case 'KARMA_CHANGE': {
|
||||||
|
if (!notification.aboutKarmaTransaction) return null
|
||||||
|
return notification.aboutKarmaTransaction.description
|
||||||
|
}
|
||||||
case 'SUGGESTION_STATUS_CHANGE':
|
case 'SUGGESTION_STATUS_CHANGE':
|
||||||
case 'ACCOUNT_STATUS_CHANGE':
|
case 'ACCOUNT_STATUS_CHANGE':
|
||||||
case 'SERVICE_VERIFICATION_STATUS_CHANGE': {
|
case 'SERVICE_VERIFICATION_STATUS_CHANGE': {
|
||||||
@@ -280,6 +303,9 @@ export function makeNotificationLink(
|
|||||||
// case 'KARMA_UNLOCK': {
|
// case 'KARMA_UNLOCK': {
|
||||||
// return `${origin}/account#karma-unlocks`
|
// return `${origin}/account#karma-unlocks`
|
||||||
// }
|
// }
|
||||||
|
case 'KARMA_CHANGE': {
|
||||||
|
return `${origin}/account#karma-transactions`
|
||||||
|
}
|
||||||
case 'ACCOUNT_STATUS_CHANGE': {
|
case 'ACCOUNT_STATUS_CHANGE': {
|
||||||
return `${origin}/account#account-status`
|
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="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="space-y-3">
|
<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>
|
||||||
|
|
||||||
{
|
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
|
||||||
sortBy(
|
{
|
||||||
karmaUnlocks.filter((unlock) => unlock.karma >= 0),
|
sortBy(
|
||||||
'karma'
|
karmaUnlocks.filter((unlock) => unlock.karma >= 0),
|
||||||
).map((unlock) => (
|
'karma'
|
||||||
<div
|
).map((unlock) => (
|
||||||
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
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex items-center justify-between rounded-md border p-3',
|
'flex items-center justify-between rounded-md border p-3',
|
||||||
user.karmaUnlocks[unlock.id]
|
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'
|
: 'border-night-500 bg-night-800'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<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" />
|
<Icon name={unlock.icon} class="size-5" />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
class={cn(
|
class={cn(
|
||||||
'font-medium',
|
'font-medium',
|
||||||
user.karmaUnlocks[unlock.id] ? 'text-red-400' : 'text-day-400'
|
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{unlock.name}
|
{unlock.name}
|
||||||
@@ -614,24 +575,85 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{user.karmaUnlocks[unlock.id] ? (
|
{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">
|
<span class="bg-day-500/20 text-day-300 inline-flex items-center rounded-full px-2 py-1 text-xs">
|
||||||
<Icon name="ri:alert-line" class="mr-1 size-3" /> Active
|
<Icon name="ri:check-line" class="mr-1 size-3" /> Unlocked
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span class="bg-night-800 text-day-400 inline-flex items-center rounded-full px-2 py-1 text-xs">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="space-y-3">
|
||||||
<Icon name="ri:information-line" class="inline-block size-4" />
|
<input type="checkbox" id="negative-unlocks-toggle" class="peer sr-only md:hidden" />
|
||||||
Negative karma leads to restrictions. <br class="hidden sm:block" />Keep interactions positive to
|
|
||||||
avoid penalties.
|
<label
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -858,7 +880,10 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
}
|
}
|
||||||
</section>
|
</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">
|
<header class="flex items-center justify-between">
|
||||||
<h2 class="font-title text-day-200 mb-4 text-xl font-bold">
|
<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" />
|
<Icon name="ri:exchange-line" class="mr-2 inline-block size-5" />
|
||||||
|
|||||||
@@ -124,6 +124,13 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast
|
|||||||
verificationStatus: true,
|
verificationStatus: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
aboutKarmaTransaction: {
|
||||||
|
select: {
|
||||||
|
points: true,
|
||||||
|
action: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
@@ -136,6 +143,7 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast
|
|||||||
enableOnMyCommentStatusChange: true,
|
enableOnMyCommentStatusChange: true,
|
||||||
enableAutowatchMyComments: true,
|
enableAutowatchMyComments: true,
|
||||||
enableNotifyPendingRepliesOnWatch: true,
|
enableNotifyPendingRepliesOnWatch: true,
|
||||||
|
karmaNotificationThreshold: true,
|
||||||
}),
|
}),
|
||||||
null,
|
null,
|
||||||
],
|
],
|
||||||
@@ -326,6 +334,23 @@ const notifications = dbNotifications.map((notification) => ({
|
|||||||
</label>
|
</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">
|
<div class="mt-4 flex justify-end">
|
||||||
<Button type="submit" label="Save" icon="ri:save-line" color="success" />
|
<Button type="submit" label="Save" icon="ri:save-line" color="success" />
|
||||||
</div>
|
</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="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="space-y-3">
|
<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>
|
||||||
|
|
||||||
{
|
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
|
||||||
sortBy(
|
{
|
||||||
karmaUnlocks.filter((unlock) => unlock.karma >= 0),
|
sortBy(
|
||||||
'karma'
|
karmaUnlocks.filter((unlock) => unlock.karma >= 0),
|
||||||
).map((unlock) => (
|
'karma'
|
||||||
<div
|
).map((unlock) => (
|
||||||
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
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex items-center justify-between rounded-md border p-3',
|
'flex items-center justify-between rounded-md border p-3',
|
||||||
user.karmaUnlocks[unlock.id]
|
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'
|
: 'border-night-500 bg-night-800'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<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" />
|
<Icon name={unlock.icon} class="size-5" />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
class={cn(
|
class={cn(
|
||||||
'font-medium',
|
'font-medium',
|
||||||
user.karmaUnlocks[unlock.id] ? 'text-red-400' : 'text-day-400'
|
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{unlock.name}
|
{unlock.name}
|
||||||
@@ -712,24 +673,85 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{user.karmaUnlocks[unlock.id] ? (
|
{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">
|
<span class="bg-day-500/20 text-day-300 inline-flex items-center rounded-full px-2 py-1 text-xs">
|
||||||
<Icon name="ri:alert-line" class="mr-1 size-3" /> Active
|
<Icon name="ri:check-line" class="mr-1 size-3" /> Unlocked
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span class="bg-night-800 text-day-400 inline-flex items-center rounded-full px-2 py-1 text-xs">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="space-y-3">
|
||||||
<Icon name="ri:information-line" class="inline-block size-4" />
|
<input type="checkbox" id="negative-unlocks-toggle" class="peer sr-only md:hidden" />
|
||||||
Negative karma leads to restrictions. <br class="hidden sm:block" />Keep interactions positive to
|
|
||||||
avoid penalties.
|
<label
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 {
|
@theme {
|
||||||
--ease-in-sine: cubic-bezier(0.12, 0, 0.39, 0);
|
--ease-in-sine: cubic-bezier(0.12, 0, 0.39, 0);
|
||||||
--ease-out-sine: cubic-bezier(0.61, 1, 0.88, 1);
|
--ease-out-sine: cubic-bezier(0.61, 1, 0.88, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user