Release 202507231133
This commit is contained in:
43
web/package-lock.json
generated
43
web/package-lock.json
generated
@@ -28,6 +28,8 @@
|
|||||||
"astro-seo-schema": "5.0.0",
|
"astro-seo-schema": "5.0.0",
|
||||||
"canvas": "3.1.2",
|
"canvas": "3.1.2",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
|
"countries-list": "3.1.1",
|
||||||
|
"country-flag-icons": "1.5.19",
|
||||||
"he": "1.2.0",
|
"he": "1.2.0",
|
||||||
"htmx.org": "2.0.6",
|
"htmx.org": "2.0.6",
|
||||||
"javascript-time-ago": "2.5.11",
|
"javascript-time-ago": "2.5.11",
|
||||||
@@ -2680,19 +2682,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.15.1",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
||||||
|
"version": "0.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||||
|
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json-schema": "^7.0.15"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@faker-js/faker": {
|
"node_modules/@faker-js/faker": {
|
||||||
"version": "9.9.0",
|
"version": "9.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
||||||
@@ -7884,6 +7899,18 @@
|
|||||||
"url": "https://opencollective.com/core-js"
|
"url": "https://opencollective.com/core-js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/countries-list": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-nPklKJ5qtmY5MdBKw1NiBAoyx5Sa7p2yPpljZyQ7gyCN1m+eMFs9I6CT37Mxt8zvR5L3VzD3DJBE4WQzX3WF4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/country-flag-icons": {
|
||||||
|
"version": "1.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.19.tgz",
|
||||||
|
"integrity": "sha512-D/ZkRyj+ywJC6b2IrAN3/tpbReMUqmuRLlcKFoY/o0+EPQN9Ev/e8tV+D3+9scvu/tarxwLErNwS73C3yzxs/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-fetch": {
|
"node_modules/cross-fetch": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
||||||
@@ -10064,14 +10091,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
"astro-seo-schema": "5.0.0",
|
"astro-seo-schema": "5.0.0",
|
||||||
"canvas": "3.1.2",
|
"canvas": "3.1.2",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
|
"countries-list": "3.1.1",
|
||||||
|
"country-flag-icons": "1.5.19",
|
||||||
"he": "1.2.0",
|
"he": "1.2.0",
|
||||||
"htmx.org": "2.0.6",
|
"htmx.org": "2.0.6",
|
||||||
"javascript-time-ago": "2.5.11",
|
"javascript-time-ago": "2.5.11",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "registrationCountryCode" VARCHAR(2);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "registeredCompanyName" TEXT;
|
||||||
@@ -345,60 +345,64 @@ model ServiceSuggestionMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Service {
|
model Service {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
slug String @unique
|
slug String @unique
|
||||||
previousSlugs String[] @default([])
|
previousSlugs String[] @default([])
|
||||||
description String
|
description String
|
||||||
categories Category[] @relation("ServiceToCategory")
|
categories Category[] @relation("ServiceToCategory")
|
||||||
kycLevel Int @default(4)
|
kycLevel Int @default(4)
|
||||||
kycLevelClarification KycLevelClarification @default(NONE)
|
kycLevelClarification KycLevelClarification @default(NONE)
|
||||||
/// Date only, no time.
|
/// Date only, no time.
|
||||||
operatingSince DateTime? @db.Date
|
operatingSince DateTime? @db.Date
|
||||||
overallScore Int @default(0)
|
overallScore Int @default(0)
|
||||||
privacyScore Int @default(0)
|
privacyScore Int @default(0)
|
||||||
trustScore Int @default(0)
|
trustScore Int @default(0)
|
||||||
/// Computed via trigger. Do not update through prisma.
|
/// Computed via trigger. Do not update through prisma.
|
||||||
averageUserRating Float?
|
averageUserRating Float?
|
||||||
serviceVisibility ServiceVisibility @default(PUBLIC)
|
serviceVisibility ServiceVisibility @default(PUBLIC)
|
||||||
serviceInfoBanner ServiceInfoBanner @default(NONE)
|
serviceInfoBanner ServiceInfoBanner @default(NONE)
|
||||||
serviceInfoBannerNotes String?
|
serviceInfoBannerNotes String?
|
||||||
verificationStatus VerificationStatus @default(COMMUNITY_CONTRIBUTED)
|
verificationStatus VerificationStatus @default(COMMUNITY_CONTRIBUTED)
|
||||||
verificationSummary String?
|
verificationSummary String?
|
||||||
verificationRequests ServiceVerificationRequest[]
|
verificationRequests ServiceVerificationRequest[]
|
||||||
verificationProofMd String?
|
verificationProofMd String?
|
||||||
/// [UserSentiment]
|
/// [UserSentiment]
|
||||||
userSentiment Json?
|
userSentiment Json?
|
||||||
userSentimentAt DateTime?
|
userSentimentAt DateTime?
|
||||||
referral String?
|
referral String?
|
||||||
acceptedCurrencies Currency[] @default([])
|
acceptedCurrencies Currency[] @default([])
|
||||||
serviceUrls String[]
|
serviceUrls String[]
|
||||||
tosUrls String[] @default([])
|
tosUrls String[] @default([])
|
||||||
onionUrls String[] @default([])
|
onionUrls String[] @default([])
|
||||||
i2pUrls String[] @default([])
|
i2pUrls String[] @default([])
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
|
/// ISO 3166-1 alpha-2 country code where the service company is registered
|
||||||
|
registrationCountryCode String? @db.VarChar(2)
|
||||||
|
/// Official name of the registered company
|
||||||
|
registeredCompanyName String?
|
||||||
/// [TosReview]
|
/// [TosReview]
|
||||||
tosReview Json?
|
tosReview Json?
|
||||||
tosReviewAt DateTime?
|
tosReviewAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
/// Computed via trigger when the visibility is PUBLIC or (ARCHIVED and listedAt was null). Do not update through prisma.
|
/// Computed via trigger when the visibility is PUBLIC or (ARCHIVED and listedAt was null). Do not update through prisma.
|
||||||
listedAt DateTime?
|
listedAt DateTime?
|
||||||
/// Computed via trigger when the verification status is APPROVED. Do not update through prisma.
|
/// Computed via trigger when the verification status is APPROVED. Do not update through prisma.
|
||||||
approvedAt DateTime?
|
approvedAt DateTime?
|
||||||
/// Computed via trigger when the verification status is VERIFICATION_SUCCESS. Do not update through prisma.
|
/// Computed via trigger when the verification status is VERIFICATION_SUCCESS. Do not update through prisma.
|
||||||
verifiedAt DateTime?
|
verifiedAt DateTime?
|
||||||
/// Computed via trigger when the verification status is VERIFICATION_FAILED. Do not update through prisma.
|
/// Computed via trigger when the verification status is VERIFICATION_FAILED. Do not update through prisma.
|
||||||
spamAt DateTime?
|
spamAt DateTime?
|
||||||
/// Computed via trigger. Do not update through prisma.
|
/// Computed via trigger. Do not update through prisma.
|
||||||
isRecentlyApproved Boolean @default(false)
|
isRecentlyApproved Boolean @default(false)
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
events Event[]
|
events Event[]
|
||||||
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
|
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
|
||||||
attributes ServiceAttribute[]
|
attributes ServiceAttribute[]
|
||||||
verificationSteps VerificationStep[]
|
verificationSteps VerificationStep[]
|
||||||
suggestions ServiceSuggestion[]
|
suggestions ServiceSuggestion[]
|
||||||
internalNotes InternalServiceNote[] @relation("ServiceRecievedNotes")
|
internalNotes InternalServiceNote[] @relation("ServiceRecievedNotes")
|
||||||
|
|
||||||
onEventCreatedForServices NotificationPreferences[] @relation("onEventCreatedForServices")
|
onEventCreatedForServices NotificationPreferences[] @relation("onEventCreatedForServices")
|
||||||
onRootCommentCreatedForServices NotificationPreferences[] @relation("onRootCommentCreatedForServices")
|
onRootCommentCreatedForServices NotificationPreferences[] @relation("onRootCommentCreatedForServices")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { generateUsername } from 'unique-username-generator'
|
|||||||
import { kycLevels } from '../src/constants/kycLevels'
|
import { kycLevels } from '../src/constants/kycLevels'
|
||||||
import { undefinedIfEmpty } from '../src/lib/arrays'
|
import { undefinedIfEmpty } from '../src/lib/arrays'
|
||||||
import { transformCase } from '../src/lib/strings'
|
import { transformCase } from '../src/lib/strings'
|
||||||
|
import { countries } from '../src/constants/countries'
|
||||||
|
|
||||||
// Exit if not in development mode
|
// Exit if not in development mode
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
@@ -697,6 +698,12 @@ const generateFakeService = (users: User[]) => {
|
|||||||
{ count: { min: 0, max: 2 } }
|
{ count: { min: 0, max: 2 } }
|
||||||
),
|
),
|
||||||
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
||||||
|
registrationCountryCode: faker.helpers.maybe(() => faker.helpers.arrayElement(countries).code, {
|
||||||
|
probability: 0.7,
|
||||||
|
}),
|
||||||
|
registeredCompanyName: faker.helpers.maybe(() => faker.company.name(), {
|
||||||
|
probability: 0.6,
|
||||||
|
}),
|
||||||
listedAt:
|
listedAt:
|
||||||
serviceVisibility === 'PUBLIC' || serviceVisibility === 'ARCHIVED'
|
serviceVisibility === 'PUBLIC' || serviceVisibility === 'ARCHIVED'
|
||||||
? faker.date.recent({ days: 30 })
|
? faker.date.recent({ days: 30 })
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ DECLARE
|
|||||||
recently_approved_factor INT := 0;
|
recently_approved_factor INT := 0;
|
||||||
tos_penalty_factor INT := 0;
|
tos_penalty_factor INT := 0;
|
||||||
operating_since_factor INT := 0;
|
operating_since_factor INT := 0;
|
||||||
|
legally_registered_factor INT := 0;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get verification status factor
|
-- Get verification status factor
|
||||||
SELECT
|
SELECT
|
||||||
@@ -160,9 +161,17 @@ BEGIN
|
|||||||
INTO operating_since_factor
|
INTO operating_since_factor
|
||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE id = service_id;
|
WHERE id = service_id;
|
||||||
|
|
||||||
|
-- Check for legal registration (country code or company name)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM "Service"
|
||||||
|
WHERE id = service_id AND ("registrationCountryCode" IS NOT NULL OR "registeredCompanyName" IS NOT NULL)
|
||||||
|
) THEN
|
||||||
|
legally_registered_factor := 2;
|
||||||
|
END IF;
|
||||||
|
|
||||||
-- Calculate final trust score (base 100)
|
-- Calculate final trust score (base 100)
|
||||||
trust_score := 50 + verification_factor + attributes_score + recently_approved_factor + tos_penalty_factor + operating_since_factor;
|
trust_score := 50 + verification_factor + attributes_score + recently_approved_factor + tos_penalty_factor + operating_since_factor + legally_registered_factor;
|
||||||
|
|
||||||
-- Ensure the score is in reasonable bounds (0-100)
|
-- Ensure the score is in reasonable bounds (0-100)
|
||||||
trust_score := GREATEST(0, LEAST(100, trust_score));
|
trust_score := GREATEST(0, LEAST(100, trust_score));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActionError } from 'astro:actions'
|
|||||||
import { uniq } from 'lodash-es'
|
import { uniq } from 'lodash-es'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
|
|
||||||
|
import { countriesZodEnumById } from '../../constants/countries'
|
||||||
import { defineProtectedAction } from '../../lib/defineProtectedAction'
|
import { defineProtectedAction } from '../../lib/defineProtectedAction'
|
||||||
import { saveFileLocally, deleteFileLocally } from '../../lib/fileStorage'
|
import { saveFileLocally, deleteFileLocally } from '../../lib/fileStorage'
|
||||||
import { prisma } from '../../lib/prisma'
|
import { prisma } from '../../lib/prisma'
|
||||||
@@ -59,6 +60,14 @@ const serviceSchemaBase = z.object({
|
|||||||
.nullable()
|
.nullable()
|
||||||
.default(null),
|
.default(null),
|
||||||
operatingSince: z.coerce.date().optional().nullable(),
|
operatingSince: z.coerce.date().optional().nullable(),
|
||||||
|
registrationCountryCode: z
|
||||||
|
.union([countriesZodEnumById, z.literal('')])
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.refine((val) => val === null || val === undefined || val === '' || val.length === 2, {
|
||||||
|
message: 'Country code must be a valid 2-character code or empty',
|
||||||
|
}),
|
||||||
|
registeredCompanyName: z.string().trim().max(100).optional().nullable(),
|
||||||
imageFile: imageFileSchema,
|
imageFile: imageFileSchema,
|
||||||
overallScore: zodCohercedNumber(z.number().int().min(0).max(10)).optional(),
|
overallScore: zodCohercedNumber(z.number().int().min(0).max(10)).optional(),
|
||||||
serviceVisibility: z.nativeEnum(ServiceVisibility),
|
serviceVisibility: z.nativeEnum(ServiceVisibility),
|
||||||
@@ -283,6 +292,8 @@ export const adminServiceActions = {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
operatingSince: input.operatingSince,
|
operatingSince: input.operatingSince,
|
||||||
|
registrationCountryCode: input.registrationCountryCode ?? null,
|
||||||
|
registeredCompanyName: input.registeredCompanyName,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from 'astro/zod'
|
|||||||
import { ActionError } from 'astro:actions'
|
import { ActionError } from 'astro:actions'
|
||||||
import { formatDistanceStrict } from 'date-fns'
|
import { formatDistanceStrict } from 'date-fns'
|
||||||
|
|
||||||
|
import { countriesZodEnumById } from '../constants/countries'
|
||||||
import { captchaFormSchemaProperties, captchaFormSchemaSuperRefine } from '../lib/captchaValidation'
|
import { captchaFormSchemaProperties, captchaFormSchemaSuperRefine } from '../lib/captchaValidation'
|
||||||
import { defineProtectedAction } from '../lib/defineProtectedAction'
|
import { defineProtectedAction } from '../lib/defineProtectedAction'
|
||||||
import { saveFileLocally } from '../lib/fileStorage'
|
import { saveFileLocally } from '../lib/fileStorage'
|
||||||
@@ -184,6 +185,14 @@ export const serviceSuggestionActions = {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
operatingSince: z.coerce.date().optional(),
|
operatingSince: z.coerce.date().optional(),
|
||||||
|
registrationCountryCode: z
|
||||||
|
.union([countriesZodEnumById, z.literal('')])
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.refine((val) => val === null || val === undefined || val === '' || val.length === 2, {
|
||||||
|
message: 'Country code must be a valid 2-character code or empty',
|
||||||
|
}),
|
||||||
|
registeredCompanyName: z.string().trim().max(100).optional(),
|
||||||
/** @deprecated Honey pot field, do not use */
|
/** @deprecated Honey pot field, do not use */
|
||||||
message: z.unknown().optional(),
|
message: z.unknown().optional(),
|
||||||
skipDuplicateCheck: z
|
skipDuplicateCheck: z
|
||||||
@@ -282,6 +291,8 @@ export const serviceSuggestionActions = {
|
|||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
operatingSince: input.operatingSince,
|
operatingSince: input.operatingSince,
|
||||||
|
registrationCountryCode: input.registrationCountryCode ?? null,
|
||||||
|
registeredCompanyName: input.registeredCompanyName,
|
||||||
serviceUrls,
|
serviceUrls,
|
||||||
tosUrls: input.tosUrls,
|
tosUrls: input.tosUrls,
|
||||||
onionUrls,
|
onionUrls,
|
||||||
|
|||||||
76
web/src/constants/countries.ts
Normal file
76
web/src/constants/countries.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { countries as countriesData, type TCountryCode } from 'countries-list'
|
||||||
|
import { countries as flagCountries } from 'country-flag-icons'
|
||||||
|
import getUnicodeFlagIcon from 'country-flag-icons/unicode'
|
||||||
|
|
||||||
|
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
||||||
|
|
||||||
|
type CountryInfo<T extends string | null | undefined = string> = {
|
||||||
|
code: T
|
||||||
|
name: string
|
||||||
|
flag: string
|
||||||
|
slug: string
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert countries-list data to our format, ensuring we only use countries that have flags
|
||||||
|
const countriesArray = Object.entries(countriesData)
|
||||||
|
.filter(([code]) => flagCountries.includes(code as TCountryCode))
|
||||||
|
.map(([code, data]) => ({
|
||||||
|
code: code as TCountryCode,
|
||||||
|
name: data.name,
|
||||||
|
flag: getUnicodeFlagIcon(code) || '🏳️',
|
||||||
|
slug: code.toLowerCase(),
|
||||||
|
order: data.name.charCodeAt(0), // Sort alphabetically by first letter
|
||||||
|
}))
|
||||||
|
// Pre-sort the array alphabetically by name for performance
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
|
// Create a map for efficient lookups
|
||||||
|
const countriesMap = new Map(countriesArray.map((country) => [country.code, country]))
|
||||||
|
|
||||||
|
export const {
|
||||||
|
dataArray: countries,
|
||||||
|
dataObject: countriesById,
|
||||||
|
getFn: getCountryInfo,
|
||||||
|
getFnSlug: getCountryInfoBySlug,
|
||||||
|
zodEnumBySlug: countriesZodEnumBySlug,
|
||||||
|
zodEnumById: countriesZodEnumById,
|
||||||
|
keyToSlug: countryCodeToSlug,
|
||||||
|
slugToKey: countrySlugToCode,
|
||||||
|
} = makeHelpersForOptions(
|
||||||
|
'code',
|
||||||
|
(code): CountryInfo<typeof code> => {
|
||||||
|
// For null, undefined, or empty string, return a default "No Country" object
|
||||||
|
if (!code) {
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
name: 'No Country',
|
||||||
|
flag: '🏳️',
|
||||||
|
slug: '',
|
||||||
|
order: 999,
|
||||||
|
} as CountryInfo<typeof code>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the country in our pre-built map
|
||||||
|
const country = countriesMap.get(code as TCountryCode)
|
||||||
|
|
||||||
|
// If found, return it; otherwise, return a default "Unknown Country" object
|
||||||
|
if (country) {
|
||||||
|
return country as CountryInfo
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
name: 'Unknown Country',
|
||||||
|
flag: '🏳️',
|
||||||
|
slug: code.toLowerCase(),
|
||||||
|
order: 999,
|
||||||
|
} as CountryInfo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
countriesArray
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to validate country code
|
||||||
|
export const isValidCountryCode = (code: string): code is TCountryCode => {
|
||||||
|
return code in countriesData && flagCountries.includes(code as TCountryCode)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { differenceInMonths, differenceInYears } from 'date-fns'
|
import { differenceInMonths, differenceInYears } from 'date-fns'
|
||||||
|
import he from 'he'
|
||||||
import { orderBy } from 'lodash-es'
|
import { orderBy } from 'lodash-es'
|
||||||
|
|
||||||
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
|
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
|
||||||
import { getAttributeTypeInfo } from '../constants/attributeTypes'
|
import { getAttributeTypeInfo } from '../constants/attributeTypes'
|
||||||
|
import { getCountryInfo } from '../constants/countries'
|
||||||
import { kycLevelClarifications } from '../constants/kycLevelClarifications'
|
import { kycLevelClarifications } from '../constants/kycLevelClarifications'
|
||||||
import { kycLevels } from '../constants/kycLevels'
|
import { kycLevels } from '../constants/kycLevels'
|
||||||
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
||||||
@@ -47,6 +49,8 @@ type NonDbAttributeFull = NonDbAttribute & {
|
|||||||
kycLevel: true
|
kycLevel: true
|
||||||
kycLevelClarification: true
|
kycLevelClarification: true
|
||||||
operatingSince: true
|
operatingSince: true
|
||||||
|
registrationCountryCode: true
|
||||||
|
registeredCompanyName: true
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
) => Partial<Pick<NonDbAttribute, 'description' | 'title'>> & {
|
) => Partial<Pick<NonDbAttribute, 'description' | 'title'>> & {
|
||||||
@@ -306,6 +310,42 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: 'legally-registered',
|
||||||
|
title: 'Legally registered',
|
||||||
|
type: 'INFO',
|
||||||
|
category: 'TRUST',
|
||||||
|
description: 'This service is legally registered as a company.',
|
||||||
|
privacyPoints: 0,
|
||||||
|
trustPoints: 2,
|
||||||
|
links: [],
|
||||||
|
customize: (service) => {
|
||||||
|
const countryCode = service.registrationCountryCode
|
||||||
|
const companyName = service.registeredCompanyName
|
||||||
|
|
||||||
|
if (!countryCode && !companyName) {
|
||||||
|
return { show: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryInfo = countryCode ? getCountryInfo(countryCode) : null
|
||||||
|
const flagTitle = countryInfo ? `${countryInfo.flag} Legally registered` : 'Legally registered'
|
||||||
|
|
||||||
|
let description = 'Legally registered.'
|
||||||
|
if (companyName && countryCode && countryInfo) {
|
||||||
|
description = `Legally registered as **${he.escape(companyName)}** in **${countryInfo.name}**.`
|
||||||
|
} else if (companyName) {
|
||||||
|
description = `Legally registered as **${he.escape(companyName)}**.`
|
||||||
|
} else if (countryCode && countryInfo) {
|
||||||
|
description = `Legally registered in **${countryInfo.name}**.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
title: flagTitle,
|
||||||
|
description,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function sortAttributes<
|
export function sortAttributes<
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import UserBadge from '../../../../components/UserBadge.astro'
|
|||||||
import { getAttributeCategoryInfo } from '../../../../constants/attributeCategories'
|
import { getAttributeCategoryInfo } from '../../../../constants/attributeCategories'
|
||||||
import { getAttributeTypeInfo } from '../../../../constants/attributeTypes'
|
import { getAttributeTypeInfo } from '../../../../constants/attributeTypes'
|
||||||
import { contactMethodUrlTypes, formatContactMethod } from '../../../../constants/contactMethods'
|
import { contactMethodUrlTypes, formatContactMethod } from '../../../../constants/contactMethods'
|
||||||
|
import { countries } from '../../../../constants/countries'
|
||||||
import { currencies } from '../../../../constants/currencies'
|
import { currencies } from '../../../../constants/currencies'
|
||||||
import { eventTypes, getEventTypeInfo } from '../../../../constants/eventTypes'
|
import { eventTypes, getEventTypeInfo } from '../../../../constants/eventTypes'
|
||||||
import { kycLevelClarifications } from '../../../../constants/kycLevelClarifications'
|
import { kycLevelClarifications } from '../../../../constants/kycLevelClarifications'
|
||||||
@@ -386,6 +387,36 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<InputText
|
||||||
|
label="Registered Company Name"
|
||||||
|
name="registeredCompanyName"
|
||||||
|
description="Official name of the registered company"
|
||||||
|
inputProps={{
|
||||||
|
value: service.registeredCompanyName,
|
||||||
|
placeholder: 'e.g. Example Corp Ltd.',
|
||||||
|
}}
|
||||||
|
error={serviceInputErrors.registeredCompanyName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputSelect
|
||||||
|
name="registrationCountryCode"
|
||||||
|
label="Company Registration Country"
|
||||||
|
description="Country where the service company is legally registered"
|
||||||
|
options={[
|
||||||
|
{ label: 'Not registered', value: '' },
|
||||||
|
...countries
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((country) => ({
|
||||||
|
label: `${country.flag} ${country.name}`,
|
||||||
|
value: country.code,
|
||||||
|
}))
|
||||||
|
]}
|
||||||
|
selectedValue={service.registrationCountryCode || ''}
|
||||||
|
error={serviceInputErrors.registrationCountryCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<InputImageFile
|
<InputImageFile
|
||||||
label="Image"
|
label="Image"
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import InputCheckbox from '../../components/InputCheckbox.astro'
|
|||||||
import InputCheckboxGroup from '../../components/InputCheckboxGroup.astro'
|
import InputCheckboxGroup from '../../components/InputCheckboxGroup.astro'
|
||||||
import InputHoneypotTrap from '../../components/InputHoneypotTrap.astro'
|
import InputHoneypotTrap from '../../components/InputHoneypotTrap.astro'
|
||||||
import InputImageFile from '../../components/InputImageFile.astro'
|
import InputImageFile from '../../components/InputImageFile.astro'
|
||||||
|
import InputSelect from '../../components/InputSelect.astro'
|
||||||
import InputSubmitButton from '../../components/InputSubmitButton.astro'
|
import InputSubmitButton from '../../components/InputSubmitButton.astro'
|
||||||
import InputText from '../../components/InputText.astro'
|
import InputText from '../../components/InputText.astro'
|
||||||
import InputTextArea from '../../components/InputTextArea.astro'
|
import InputTextArea from '../../components/InputTextArea.astro'
|
||||||
import { getAttributeCategoryInfo } from '../../constants/attributeCategories'
|
import { getAttributeCategoryInfo } from '../../constants/attributeCategories'
|
||||||
import { getAttributeTypeInfo } from '../../constants/attributeTypes'
|
import { getAttributeTypeInfo } from '../../constants/attributeTypes'
|
||||||
import { contactMethodUrlTypes } from '../../constants/contactMethods'
|
import { contactMethodUrlTypes } from '../../constants/contactMethods'
|
||||||
|
import { countries } from '../../constants/countries'
|
||||||
import { currencies } from '../../constants/currencies'
|
import { currencies } from '../../constants/currencies'
|
||||||
import { kycLevelClarifications } from '../../constants/kycLevelClarifications'
|
import { kycLevelClarifications } from '../../constants/kycLevelClarifications'
|
||||||
import { kycLevels } from '../../constants/kycLevels'
|
import { kycLevels } from '../../constants/kycLevels'
|
||||||
@@ -262,6 +264,33 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<InputText
|
||||||
|
label="Registered Company Name"
|
||||||
|
name="registeredCompanyName"
|
||||||
|
description="Official name of the registered company (optional)"
|
||||||
|
inputProps={{
|
||||||
|
placeholder: 'e.g. Example Corp Ltd.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputSelect
|
||||||
|
name="registrationCountryCode"
|
||||||
|
label="Company Registration Country"
|
||||||
|
description="Country where the service company is legally registered (optional)"
|
||||||
|
options={[
|
||||||
|
{ label: 'Not registered', value: '' },
|
||||||
|
...countries
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((country) => ({
|
||||||
|
label: `${country.flag} ${country.name}`,
|
||||||
|
value: country.code,
|
||||||
|
}))
|
||||||
|
]}
|
||||||
|
selectedValue=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<InputCardGroup
|
<InputCardGroup
|
||||||
name="kycLevel"
|
name="kycLevel"
|
||||||
label="KYC Level"
|
label="KYC Level"
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
acceptedCurrencies: true,
|
acceptedCurrencies: true,
|
||||||
operatingSince: true,
|
operatingSince: true,
|
||||||
|
registrationCountryCode: true,
|
||||||
|
registeredCompanyName: true,
|
||||||
tosReview: true,
|
tosReview: true,
|
||||||
tosReviewAt: true,
|
tosReviewAt: true,
|
||||||
userSentiment: true,
|
userSentiment: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user