Release 202507030838
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { actions } from 'astro:actions'
|
||||
import { differenceInCalendarDays } from 'date-fns'
|
||||
import { sortBy } from 'lodash-es'
|
||||
|
||||
import BadgeSmall from '../../components/BadgeSmall.astro'
|
||||
@@ -19,6 +20,7 @@ import { verificationStatusesByValue } from '../../constants/verificationStatus'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import { cn } from '../../lib/cn'
|
||||
import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks'
|
||||
import { pluralize } from '../../lib/pluralize'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||
import { formatDateShort } from '../../lib/timeAgo'
|
||||
@@ -49,6 +51,7 @@ const user = await Astro.locals.banners.try('user', async () => {
|
||||
verifiedLink: true,
|
||||
totalKarma: true,
|
||||
createdAt: true,
|
||||
scheduledDeletionAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
comments: true,
|
||||
@@ -158,6 +161,10 @@ const user = await Astro.locals.banners.try('user', async () => {
|
||||
})
|
||||
|
||||
if (!user) return Astro.rewrite('/404')
|
||||
|
||||
const daysUntilDeletion = user.scheduledDeletionAt
|
||||
? differenceInCalendarDays(user.scheduledDeletionAt, new Date())
|
||||
: null
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -394,6 +401,33 @@ if (!user) return Astro.rewrite('/404')
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
daysUntilDeletion && (
|
||||
<li class="flex items-start">
|
||||
<span class="text-day-500 mt-0.5 mr-2">
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-day-500 text-xs">Deletion Status</p>
|
||||
<span class="rounded-full border border-red-500/50 bg-red-500/20 px-2 py-0.5 text-xs text-red-400">
|
||||
Scheduled for deletion
|
||||
{daysUntilDeletion <= 0 ? (
|
||||
'today'
|
||||
) : (
|
||||
<>
|
||||
in {daysUntilDeletion.toLocaleString()} {pluralize('day', daysUntilDeletion)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<p class="text-day-400 mt-2 text-xs">
|
||||
To prevent deletion, take any action such as voting, commenting, or suggesting a
|
||||
service.
|
||||
</p>
|
||||
</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>
|
||||
|
||||
@@ -28,6 +28,11 @@ There are several ways to earn karma points:
|
||||
- Similarly, each downvote reduces your karma by -1.
|
||||
- This allows the community to reward helpful contributions.
|
||||
|
||||
4. **Suggestion Approval** (+10 points)
|
||||
- When your suggestion to add or edit a service is approved and the service is listed publicly.
|
||||
- Suggestions on non-listed services do not earn karma.
|
||||
- This rewards users for helping to expand and improve the service directory.
|
||||
|
||||
## Karma Penalties
|
||||
|
||||
The system also includes penalties to discourage spam and low-quality content:
|
||||
|
||||
@@ -6,20 +6,14 @@ import seedrandom from 'seedrandom'
|
||||
import Button from '../components/Button.astro'
|
||||
import InputText from '../components/InputText.astro'
|
||||
import Pagination from '../components/Pagination.astro'
|
||||
import ServiceFiltersPill from '../components/ServiceFiltersPill.astro'
|
||||
import ServiceFiltersPillsRow from '../components/ServiceFiltersPillsRow.astro'
|
||||
import ServicesFilters from '../components/ServicesFilters.astro'
|
||||
import ServicesSearchResults from '../components/ServicesSearchResults.astro'
|
||||
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
|
||||
import { getAttributeTypeInfo } from '../constants/attributeTypes'
|
||||
import { currencies, currenciesZodEnumBySlug, currencySlugToId } from '../constants/currencies'
|
||||
import { networks } from '../constants/networks'
|
||||
import {
|
||||
currencies,
|
||||
currenciesZodEnumBySlug,
|
||||
currencySlugToId,
|
||||
getCurrencyInfo,
|
||||
} from '../constants/currencies'
|
||||
import { getNetworkInfo, networks } from '../constants/networks'
|
||||
import {
|
||||
getVerificationStatusInfo,
|
||||
verificationStatuses,
|
||||
verificationStatusesZodEnumBySlug,
|
||||
verificationStatusSlugToId,
|
||||
@@ -32,7 +26,6 @@ import { areEqualObjectsWithoutOrder } from '../lib/objects'
|
||||
import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters'
|
||||
import { prisma } from '../lib/prisma'
|
||||
import { makeSortSeed } from '../lib/sortSeed'
|
||||
import { transformCase } from '../lib/strings'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
@@ -115,23 +108,29 @@ const modeOptions = [
|
||||
label: string
|
||||
}[]
|
||||
|
||||
export type AttributeOption = {
|
||||
value: string
|
||||
prefix: string
|
||||
prefixWith: string
|
||||
}
|
||||
|
||||
const attributeOptions = [
|
||||
{
|
||||
value: 'yes',
|
||||
prefix: 'Has',
|
||||
prefixWith: 'with',
|
||||
},
|
||||
{
|
||||
value: 'no',
|
||||
prefix: 'Not',
|
||||
prefixWith: 'without',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
prefix: '',
|
||||
prefixWith: '',
|
||||
},
|
||||
] as const satisfies {
|
||||
value: string
|
||||
prefix: string
|
||||
}[]
|
||||
] as const satisfies AttributeOption[]
|
||||
|
||||
const ignoredKeysForDefaultData = ['sort-seed']
|
||||
|
||||
@@ -309,6 +308,7 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
||||
prisma.category.findMany({
|
||||
select: {
|
||||
name: true,
|
||||
namePluralLong: true,
|
||||
slug: true,
|
||||
icon: true,
|
||||
_count: {
|
||||
@@ -322,6 +322,7 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
],
|
||||
[
|
||||
'Unable to load services.',
|
||||
@@ -507,7 +508,7 @@ const attributesByCategory = orderBy(
|
||||
)
|
||||
|
||||
const categoriesSorted = orderBy(
|
||||
categories?.map((category) => {
|
||||
categories.map((category) => {
|
||||
const checked = filters.categories.includes(category.slug)
|
||||
|
||||
return {
|
||||
@@ -584,114 +585,13 @@ const showFiltersId = 'show-filters'
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
<div class="-ml-4 flex flex-1 items-center gap-2 overflow-x-auto mask-r-from-[calc(100%-var(--spacing)*16)] pr-12 pl-4">
|
||||
{filters.q && (
|
||||
<ServiceFiltersPill text={`"${filters.q}"`} searchParamName="q" searchParamValue={filters.q} />
|
||||
)}
|
||||
|
||||
{!areEqualArraysWithoutOrder(
|
||||
filters.verification,
|
||||
filtersOptions.verification
|
||||
.filter((verification) => verification.default)
|
||||
.map((verification) => verification.value)
|
||||
) &&
|
||||
filters.verification.map((verificationStatus) => {
|
||||
const verificationStatusInfo = getVerificationStatusInfo(verificationStatus)
|
||||
|
||||
return (
|
||||
<ServiceFiltersPill
|
||||
text={verificationStatusInfo.label}
|
||||
icon={verificationStatusInfo.icon}
|
||||
iconClass={verificationStatusInfo.classNames.icon}
|
||||
searchParamName="verification"
|
||||
searchParamValue={verificationStatusInfo.slug}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{filters.categories.map((categorySlug) => {
|
||||
const category = categories?.find((c) => c.slug === categorySlug)
|
||||
if (!category) return null
|
||||
|
||||
return (
|
||||
<ServiceFiltersPill
|
||||
text={category.name}
|
||||
icon={category.icon}
|
||||
searchParamName="categories"
|
||||
searchParamValue={categorySlug}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{filters.currencies.map((currencyId) => {
|
||||
const currency = getCurrencyInfo(currencyId)
|
||||
|
||||
return (
|
||||
<ServiceFiltersPill
|
||||
text={currency.name}
|
||||
searchParamName="currencies"
|
||||
searchParamValue={currency.slug}
|
||||
icon={currency.icon}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{filters.networks.map((network) => {
|
||||
const networkOption = getNetworkInfo(network)
|
||||
|
||||
return (
|
||||
<ServiceFiltersPill
|
||||
text={networkOption.name}
|
||||
icon={networkOption.icon}
|
||||
searchParamName="networks"
|
||||
searchParamValue={network}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{filters['max-kyc'] < 4 && (
|
||||
<ServiceFiltersPill
|
||||
text={`KYC Lvl ≤ ${filters['max-kyc'].toLocaleString()}`}
|
||||
icon="ri:shield-keyhole-line"
|
||||
searchParamName="max-kyc"
|
||||
/>
|
||||
)}
|
||||
{filters['user-rating'] > 0 && (
|
||||
<ServiceFiltersPill
|
||||
text={`Rating ≥ ${filters['user-rating'].toLocaleString()}★`}
|
||||
icon="ri:star-fill"
|
||||
searchParamName="user-rating"
|
||||
/>
|
||||
)}
|
||||
{filters['min-score'] > 0 && (
|
||||
<ServiceFiltersPill
|
||||
text={`Score ≥ ${filters['min-score'].toLocaleString()}`}
|
||||
icon="ri:medal-line"
|
||||
searchParamName="min-score"
|
||||
/>
|
||||
)}
|
||||
{filters['attribute-mode'] === 'and' && filters.attr && Object.keys(filters.attr).length > 1 && (
|
||||
<ServiceFiltersPill
|
||||
text="Attributes: AND"
|
||||
icon="ri:filter-3-line"
|
||||
searchParamName="attribute-mode"
|
||||
searchParamValue="and"
|
||||
/>
|
||||
)}
|
||||
{filters.attr &&
|
||||
Object.entries(filters.attr)
|
||||
.filter((entry): entry is [string, 'no' | 'yes'] => entry[1] === 'yes' || entry[1] === 'no')
|
||||
.map(([attributeId, attributeValue]) => {
|
||||
const attribute = attributes.find((attr) => String(attr.id) === attributeId)
|
||||
if (!attribute) return null
|
||||
const valueInfo = attributeOptions.find((option) => option.value === attributeValue)
|
||||
const prefix = valueInfo?.prefix ?? transformCase(attributeValue, 'title')
|
||||
return (
|
||||
<ServiceFiltersPill
|
||||
text={`${prefix}: ${attribute.title}`}
|
||||
searchParamName={`attr-${attributeId}`}
|
||||
searchParamValue={attributeValue}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<ServiceFiltersPillsRow
|
||||
filters={filters}
|
||||
filtersOptions={filtersOptions}
|
||||
categories={categories}
|
||||
attributes={attributes}
|
||||
attributeOptions={attributeOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -739,6 +639,10 @@ const showFiltersId = 'show-filters'
|
||||
filters={filters}
|
||||
countCommunityOnly={countCommunityOnly}
|
||||
inlineIcons
|
||||
categories={categories}
|
||||
filtersOptions={filtersOptions}
|
||||
attributes={attributes}
|
||||
attributeOptions={attributeOptions}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user