Compare commits

...

2 Commits

Author SHA1 Message Date
pluja
99bc1f4e0f Release 202507031129 2025-07-03 11:29:46 +00:00
pluja
3166349dfb Release 202507031117 2025-07-03 11:17:39 +00:00
3 changed files with 150 additions and 175 deletions

View File

@@ -275,7 +275,7 @@ CREATE OR REPLACE FUNCTION handle_suggestion_status_change()
RETURNS TRIGGER AS $$
DECLARE
service_name TEXT;
service_visibility "ServiceVisibility";
service_visibility "serviceVisibility";
is_user_admin_or_moderator BOOLEAN;
BEGIN
-- Award karma for first approval
@@ -283,7 +283,7 @@ BEGIN
-- and ensure it wasn't already APPROVED.
IF OLD.status IS DISTINCT FROM 'APPROVED' AND NEW.status = 'APPROVED' THEN
-- Fetch service details for the description
SELECT name, visibility INTO service_name, service_visibility FROM "Service" WHERE id = NEW."serviceId";
SELECT name, serviceVisibility INTO service_name, service_visibility FROM "Service" WHERE id = NEW."serviceId";
-- Only award karma if the service is public
IF service_visibility = 'PUBLIC' THEN

View File

@@ -5,6 +5,7 @@ import { Icon } from 'astro-icon/components'
import BadgeSmall from '../../components/BadgeSmall.astro'
import CommentModeration from '../../components/CommentModeration.astro'
import MyPicture from '../../components/MyPicture.astro'
import Pagination from '../../components/Pagination.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
import UserBadge from '../../components/UserBadge.astro'
import {
@@ -27,7 +28,7 @@ if (!user || (!user.admin && !user.moderator)) {
const { data: params } = zodParseQueryParamsStoringErrors(
{
status: commentStatusFiltersZodEnum.default('all'),
page: z.number().int().positive().default(1),
page: z.coerce.number().int().positive().default(1),
},
Astro
)
@@ -241,29 +242,5 @@ const totalPages = Math.ceil(totalComments / PAGE_SIZE)
</div>
<!-- Pagination -->
{
totalPages > 1 && (
<div class="mt-8 flex justify-center gap-2">
{params.page > 1 && (
<a
href={urlWithParams(Astro.url, { page: params.page - 1 })}
class="font-title rounded-md border border-zinc-700 px-3 py-1 text-sm transition-colors hover:border-green-500/50"
>
Previous
</a>
)}
<span class="font-title px-3 py-1 text-sm">
Page {params.page} of {totalPages}
</span>
{params.page < totalPages && (
<a
href={urlWithParams(Astro.url, { page: params.page + 1 })}
class="font-title rounded-md border border-zinc-700 px-3 py-1 text-sm transition-colors hover:border-green-500/50"
>
Next
</a>
)}
</div>
)
}
{totalPages > 1 && <Pagination currentPage={params.page} totalPages={totalPages} class="mt-8" />}
</BaseLayout>

View File

@@ -7,186 +7,184 @@ import { makeSearchFiltersOptions } from '../../lib/searchFiltersOptions'
import type { APIRoute } from 'astro'
export const GET: APIRoute = async ({ site }) => {
if (!site) {
return new Response('Site URL not configured', { status: 500 })
}
if (!site) return new Response('Site URL not configured', { status: 500 })
const searchUrls = await generateSEOSitemapUrls(site.href)
try {
const searchUrls = await generateSEOSitemapUrls(site.href)
const result = `
const result = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${searchUrls.map((url) => `<url><loc>${he.encode(url)}</loc></url>`).join('\n')}
</urlset>
`.trim()
return new Response(result, {
headers: {
'Content-Type': 'application/xml',
},
})
return new Response(result, {
headers: {
'Content-Type': 'application/xml',
},
})
} catch (error) {
console.error('Failed to generate SEO sitemap URLs:', error)
return new Response('Failed to generate SEO sitemap URLs', { status: 500 })
}
}
async function generateSEOSitemapUrls(siteUrl: string) {
try {
const [categories, attributes] = await Promise.all([
prisma.category.findMany({
select: {
name: true,
namePluralLong: true,
slug: true,
icon: true,
_count: {
select: {
services: {
where: {
const [categories, attributes] = await Promise.all([
prisma.category.findMany({
select: {
name: true,
namePluralLong: true,
slug: true,
icon: true,
_count: {
select: {
services: {
where: {
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
},
},
},
},
},
}),
prisma.attribute.findMany({
select: {
id: true,
slug: true,
title: true,
category: true,
type: true,
_count: {
select: {
services: {
where: {
service: {
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
},
},
},
},
},
}),
prisma.attribute.findMany({
select: {
id: true,
slug: true,
title: true,
category: true,
type: true,
_count: {
select: {
services: {
where: {
service: {
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
},
},
},
},
},
},
orderBy: [{ category: 'asc' }, { type: 'asc' }, { title: 'asc' }],
}),
])
},
orderBy: [{ category: 'asc' }, { type: 'asc' }, { title: 'asc' }],
}),
])
const filtersOptions = makeSearchFiltersOptions({
filters: null,
categories,
attributes,
})
const filtersOptions = makeSearchFiltersOptions({
filters: null,
categories,
attributes,
})
const byCategory = filtersOptions.categories.map(
(category) =>
const byCategory = filtersOptions.categories.map(
(category) =>
new URLSearchParams({
categories: category.slug,
})
)
const byVerificationStatus = filtersOptions.verification.map(
(status) =>
new URLSearchParams({
verification: status.slug,
})
)
const byKycLevel = filtersOptions.kycLevels.map(
(level) =>
new URLSearchParams({
'max-kyc': level.id,
})
)
const byCurrency = filtersOptions.currencies.map(
(currency) =>
new URLSearchParams({
currencies: currency.slug,
})
)
const withOneAttribute = filtersOptions.attributesByCategory
.flatMap(({ attributes }) => attributes)
.map(
(attribute) =>
new URLSearchParams({
categories: category.slug,
[`attr-${attribute.id.toString()}`]: 'yes',
})
)
const withoutOneAttribute = filtersOptions.attributesByCategory
.flatMap(({ attributes }) => attributes)
.map(
(attribute) =>
new URLSearchParams({
[`attr-${attribute.id.toString()}`]: 'no',
})
)
const byVerificationStatus = filtersOptions.verification.map(
(status) =>
new URLSearchParams({
verification: status.slug,
})
)
const byKycLevel = filtersOptions.kycLevels.map(
(level) =>
new URLSearchParams({
'max-kyc': level.id,
})
)
const byCurrency = filtersOptions.currencies.map(
const byCategoryAndCurrency = filtersOptions.categories.flatMap((category) =>
filtersOptions.currencies.map(
(currency) =>
new URLSearchParams({
categories: category.slug,
currencies: currency.slug,
})
)
)
const withOneAttribute = filtersOptions.attributesByCategory
const byCategoryAndAttributes = filtersOptions.categories.flatMap((category) =>
filtersOptions.attributesByCategory
.flatMap(({ attributes }) => attributes)
.map(
(attribute) =>
new URLSearchParams({
[`attr-${attribute.id.toString()}`]: 'yes',
})
)
const withoutOneAttribute = filtersOptions.attributesByCategory
.flatMap((attribute) => [
new URLSearchParams({
categories: category.slug,
[`attr-${attribute.id.toString()}`]: 'yes',
}),
new URLSearchParams({
categories: category.slug,
[`attr-${attribute.id.toString()}`]: 'no',
}),
])
)
const relevantCurrencies = [
'xmr',
'btc',
] as const satisfies (typeof filtersOptions.currencies)[number]['slug'][]
const byCategoryAndAttributesAndRelevantCurrency = filtersOptions.categories.flatMap((category) =>
filtersOptions.attributesByCategory
.flatMap(({ attributes }) => attributes)
.map(
(attribute) =>
new URLSearchParams({
[`attr-${attribute.id.toString()}`]: 'no',
})
)
const byCategoryAndCurrency = filtersOptions.categories.flatMap((category) =>
filtersOptions.currencies.map(
(currency) =>
new URLSearchParams({
categories: category.slug,
currencies: currency.slug,
})
)
)
const byCategoryAndAttributes = filtersOptions.categories.flatMap((category) =>
filtersOptions.attributesByCategory
.flatMap(({ attributes }) => attributes)
.flatMap((attribute) => [
new URLSearchParams({
categories: category.slug,
[`attr-${attribute.id.toString()}`]: 'yes',
}),
new URLSearchParams({
categories: category.slug,
[`attr-${attribute.id.toString()}`]: 'no',
}),
])
)
const relevantCurrencies = [
'xmr',
'btc',
] as const satisfies (typeof filtersOptions.currencies)[number]['slug'][]
const byCategoryAndAttributesAndRelevantCurrency = filtersOptions.categories.flatMap((category) =>
filtersOptions.attributesByCategory
.flatMap(({ attributes }) => attributes)
.flatMap((attribute) =>
relevantCurrencies.map(
(currency) =>
new URLSearchParams({
categories: category.slug,
currencies: currency,
[`attr-${attribute.id.toString()}`]:
attribute.type === 'GOOD' || attribute.type === 'INFO' ? 'yes' : 'no',
})
)
.flatMap((attribute) =>
relevantCurrencies.map(
(currency) =>
new URLSearchParams({
categories: category.slug,
currencies: currency,
[`attr-${attribute.id.toString()}`]:
attribute.type === 'GOOD' || attribute.type === 'INFO' ? 'yes' : 'no',
})
)
)
)
)
const allQueryParams = [
...byCategory,
...byVerificationStatus,
...byKycLevel,
...byCurrency,
...withOneAttribute,
...withoutOneAttribute,
const allQueryParams = [
...byCategory,
...byVerificationStatus,
...byKycLevel,
...byCurrency,
...withOneAttribute,
...withoutOneAttribute,
...byCategoryAndCurrency,
...byCategoryAndAttributes,
...byCategoryAndAttributesAndRelevantCurrency,
] satisfies URLSearchParams[]
...byCategoryAndCurrency,
...byCategoryAndAttributes,
...byCategoryAndAttributesAndRelevantCurrency,
] satisfies URLSearchParams[]
return allQueryParams.map((queryParams) => {
const url = new URL(siteUrl)
url.search = queryParams.toString()
return url.href
})
} catch (error) {
console.error('Failed to generate SEO sitemap URLs:', error)
return []
}
return allQueryParams.map((queryParams) => {
const url = new URL(siteUrl)
url.search = queryParams.toString()
return url.href
})
}