Compare commits
13 Commits
release-70
...
release-84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a9f5f5e99 | ||
|
|
e6edee2dbe | ||
|
|
c7ee1606e4 | ||
|
|
f3c9b92ddb | ||
|
|
effb6689d7 | ||
|
|
cf5f3b3228 | ||
|
|
5a41816ac8 | ||
|
|
bf30a6cb2b | ||
|
|
4ca9b9a5c2 | ||
|
|
03abdef4f1 | ||
|
|
d9880fd83d | ||
|
|
39afcad089 | ||
|
|
99cb730bc0 |
@@ -96,7 +96,7 @@ Tasks will run according to their configured cron schedules.
|
|||||||
### Force Triggers Task
|
### Force Triggers Task
|
||||||
|
|
||||||
- Maintains database triggers by forcing them to run under certain conditions
|
- Maintains database triggers by forcing them to run under certain conditions
|
||||||
- Currently handles updating the "isRecentlyListed" flag for services after 15 days
|
- Currently handles updating the "isRecentlyApproved" flag for services after 15 days
|
||||||
- Scheduled via `CRON_FORCE-TRIGGERS_TASK`
|
- Scheduled via `CRON_FORCE-TRIGGERS_TASK`
|
||||||
|
|
||||||
### Service Score Recalculation Task
|
### Service Score Recalculation Task
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ def parse_args(args: List[str]) -> argparse.Namespace:
|
|||||||
score_recalc_parser.add_argument(
|
score_recalc_parser.add_argument(
|
||||||
"--service-id", type=int, help="Specific service ID to process (optional)"
|
"--service-id", type=int, help="Specific service ID to process (optional)"
|
||||||
)
|
)
|
||||||
|
score_recalc_parser.add_argument(
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
help="Recalculate scores for all services (ignores --service-id)",
|
||||||
|
)
|
||||||
|
|
||||||
return parser.parse_args(args)
|
return parser.parse_args(args)
|
||||||
|
|
||||||
@@ -295,12 +300,15 @@ def run_force_triggers_task() -> int:
|
|||||||
close_db_pool()
|
close_db_pool()
|
||||||
|
|
||||||
|
|
||||||
def run_service_score_recalc_task(service_id: Optional[int] = None) -> int:
|
def run_service_score_recalc_task(
|
||||||
|
service_id: Optional[int] = None, all_services: bool = False
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Run the service score recalculation task.
|
Run the service score recalculation task.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_id: Optional specific service ID to process.
|
service_id: Optional specific service ID to process.
|
||||||
|
all_services: Whether to recalculate scores for all services.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Exit code.
|
Exit code.
|
||||||
@@ -310,7 +318,34 @@ def run_service_score_recalc_task(service_id: Optional[int] = None) -> int:
|
|||||||
try:
|
try:
|
||||||
# Initialize task and use as context manager
|
# Initialize task and use as context manager
|
||||||
with ServiceScoreRecalculationTask() as task: # type: ignore
|
with ServiceScoreRecalculationTask() as task: # type: ignore
|
||||||
result = task.run(service_id) # type: ignore
|
if all_services:
|
||||||
|
queued = task.recalculate_all_services() # type: ignore
|
||||||
|
if not queued:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to queue recalculation jobs for all services"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Continuously process queued jobs in batches until none remain
|
||||||
|
while True:
|
||||||
|
_ = task.run() # type: ignore
|
||||||
|
|
||||||
|
# Check if there are still unprocessed jobs
|
||||||
|
remaining = 0
|
||||||
|
if task.conn:
|
||||||
|
with task.conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
'SELECT COUNT(*) FROM "ServiceScoreRecalculationJob" WHERE "processedAt" IS NULL'
|
||||||
|
)
|
||||||
|
remaining = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if remaining == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
result = True # All jobs processed successfully
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = task.run(service_id) # type: ignore
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
logger.info("Successfully recalculated service scores")
|
logger.info("Successfully recalculated service scores")
|
||||||
else:
|
else:
|
||||||
@@ -366,7 +401,7 @@ def run_worker_mode() -> int:
|
|||||||
|
|
||||||
# Register service score recalculation task (every 5 minutes)
|
# Register service score recalculation task (every 5 minutes)
|
||||||
scheduler.register_task(
|
scheduler.register_task(
|
||||||
"service-score-recalc",
|
"service_score_recalc",
|
||||||
"*/5 * * * *",
|
"*/5 * * * *",
|
||||||
run_service_score_recalc_task,
|
run_service_score_recalc_task,
|
||||||
)
|
)
|
||||||
@@ -419,7 +454,9 @@ def main() -> int:
|
|||||||
elif args.task == "force-triggers":
|
elif args.task == "force-triggers":
|
||||||
return run_force_triggers_task()
|
return run_force_triggers_task()
|
||||||
elif args.task == "service-score-recalc":
|
elif args.task == "service-score-recalc":
|
||||||
return run_service_score_recalc_task(args.service_id)
|
return run_service_score_recalc_task(
|
||||||
|
args.service_id, getattr(args, "all", False)
|
||||||
|
)
|
||||||
elif args.task:
|
elif args.task:
|
||||||
logger.error(f"Unknown task: {args.task}")
|
logger.error(f"Unknown task: {args.task}")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -333,28 +333,32 @@ def remove_service_attribute_by_slug(service_id: int, attribute_slug: str) -> bo
|
|||||||
|
|
||||||
|
|
||||||
def save_tos_review(service_id: int, review: Optional[TosReviewType]):
|
def save_tos_review(service_id: int, review: Optional[TosReviewType]):
|
||||||
"""
|
"""Persist a TOS review and/or update the timestamp for a service.
|
||||||
Save a TOS review for a specific service.
|
|
||||||
|
|
||||||
Args:
|
If *review* is ``None`` the existing review (if any) is preserved while
|
||||||
service_id: The ID of the service.
|
only the ``tosReviewAt`` column is updated. This ensures we still track
|
||||||
review: A TypedDict containing the review data.
|
when the review task last ran even if the review generation failed or
|
||||||
|
produced no changes.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Only serialize to JSON if review is not None
|
|
||||||
review_json = json.dumps(review) if review is not None else None
|
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
with conn.cursor(row_factory=dict_row) as cursor:
|
with conn.cursor(row_factory=dict_row) as cursor:
|
||||||
cursor.execute(
|
if review is None:
|
||||||
"""
|
cursor.execute(
|
||||||
UPDATE "Service"
|
'UPDATE "Service" SET "tosReviewAt" = NOW() WHERE id = %s AND "tosReview" IS NULL',
|
||||||
SET "tosReview" = %s, "tosReviewAt" = NOW()
|
(service_id,),
|
||||||
WHERE id = %s
|
)
|
||||||
""",
|
else:
|
||||||
(review_json, service_id),
|
review_json = json.dumps(review)
|
||||||
)
|
cursor.execute(
|
||||||
|
'UPDATE "Service" SET "tosReview" = %s, "tosReviewAt" = NOW() WHERE id = %s',
|
||||||
|
(review_json, service_id),
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info(f"Successfully saved TOS review for service {service_id}")
|
logger.info(
|
||||||
|
f"Successfully saved TOS review (updated={review is not None}) for service {service_id}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving TOS review for service {service_id}: {e}")
|
logger.error(f"Error saving TOS review for service {service_id}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ForceTriggersTask(Task):
|
|||||||
Force triggers to run under certain conditions.
|
Force triggers to run under certain conditions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RECENT_LISTED_INTERVAL_DAYS = 15
|
RECENT_APPROVED_INTERVAL_DAYS = 15
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("force_triggers")
|
super().__init__("force_triggers")
|
||||||
@@ -24,10 +24,10 @@ class ForceTriggersTask(Task):
|
|||||||
|
|
||||||
update_query = f"""
|
update_query = f"""
|
||||||
UPDATE "Service"
|
UPDATE "Service"
|
||||||
SET "isRecentlyListed" = FALSE, "updatedAt" = NOW()
|
SET "isRecentlyApproved" = FALSE, "updatedAt" = NOW()
|
||||||
WHERE "isRecentlyListed" = TRUE
|
WHERE "isRecentlyApproved" = TRUE
|
||||||
AND "listedAt" IS NOT NULL
|
AND "approvedAt" IS NOT NULL
|
||||||
AND "listedAt" < NOW() - INTERVAL '{self.RECENT_LISTED_INTERVAL_DAYS} days'
|
AND "approvedAt" < NOW() - INTERVAL '{self.RECENT_APPROVED_INTERVAL_DAYS} days'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with self.conn.cursor() as cursor:
|
with self.conn.cursor() as cursor:
|
||||||
|
|||||||
@@ -205,8 +205,7 @@ class ServiceScoreRecalculationTask(Task):
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE "isActive" = TRUE
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
services = cursor.fetchall()
|
services = cursor.fetchall()
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class TosReviewTask(Task):
|
|||||||
service_name = service["name"]
|
service_name = service["name"]
|
||||||
verification_status = service.get("verificationStatus")
|
verification_status = service.get("verificationStatus")
|
||||||
|
|
||||||
# Only process verified or approved services
|
# Only process verified, approved, or community contributed services
|
||||||
if verification_status not in ["VERIFICATION_SUCCESS", "APPROVED"]:
|
if verification_status not in ["VERIFICATION_SUCCESS", "APPROVED", "COMMUNITY_CONTRIBUTED"]:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Skipping TOS review for service: {service_name} (ID: {service_id}) - Status: {verification_status}"
|
f"Skipping TOS review for service: {service_name} (ID: {service_id}) - Status: {verification_status}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ def prompt_check_tos_review(content: str) -> TosReviewCheck:
|
|||||||
{"role": "user", "content": content},
|
{"role": "user", "content": content},
|
||||||
]
|
]
|
||||||
|
|
||||||
result_dict = query_openai_json(messages, model="openai/gpt-4.1-mini")
|
result_dict = query_openai_json(
|
||||||
|
messages, model="openai/gemini-2.5-flash-preview-05-20"
|
||||||
|
)
|
||||||
|
|
||||||
return cast(TosReviewCheck, result_dict)
|
return cast(TosReviewCheck, result_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ export default defineConfig({
|
|||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'KYCnot.me',
|
name: 'KYCnot.me',
|
||||||
|
short_name: 'KYCnot.me',
|
||||||
description: 'Find services that respect your privacy',
|
description: 'Find services that respect your privacy',
|
||||||
theme_color: '#040505',
|
theme_color: '#040505',
|
||||||
background_color: '#171c1b',
|
background_color: '#171c1b',
|
||||||
|
display: 'minimal-ui',
|
||||||
},
|
},
|
||||||
pwaAssets: {
|
pwaAssets: {
|
||||||
image: './public/favicon.svg',
|
image: './public/favicon.svg',
|
||||||
@@ -107,8 +109,9 @@ export default defineConfig({
|
|||||||
'/service/[...slug]/proof': '/service/[...slug]/#verification',
|
'/service/[...slug]/proof': '/service/[...slug]/#verification',
|
||||||
'/attribute/[...slug]': '/attributes',
|
'/attribute/[...slug]': '/attributes',
|
||||||
'/attr/[...slug]': '/attributes',
|
'/attr/[...slug]': '/attributes',
|
||||||
'/service/[...slug]/review': '/service/[...slug]#comments',
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
'/service/[...slug]/review': '/service/[...slug]#comments',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
schema: {
|
schema: {
|
||||||
@@ -122,7 +125,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
// Public URLs (can be accessed from both server and client)
|
// Public URLs (can be accessed from both server and client)
|
||||||
SOURCE_CODE_URL: envField.string({
|
SOURCE_CODE_URL: envField.string({
|
||||||
context: 'server',
|
context: 'client',
|
||||||
access: 'public',
|
access: 'public',
|
||||||
url: true,
|
url: true,
|
||||||
optional: false,
|
optional: false,
|
||||||
|
|||||||
48
web/package-lock.json
generated
@@ -2532,9 +2532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2603,9 +2603,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7196,9 +7196,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9325,9 +9325,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9399,9 +9399,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9476,9 +9476,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/brace-expansion": {
|
"node_modules/eslint/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10379,9 +10379,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11671,9 +11671,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jake/node_modules/brace-expansion": {
|
"node_modules/jake/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "VerificationStepStatus" ADD VALUE 'WARNING';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "approvedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "spamAt" TIMESTAMP(3);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `isRecentlyListed` on the `Service` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" DROP COLUMN "isRecentlyListed",
|
||||||
|
ADD COLUMN "isRecentlyApproved" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_approvedAt_idx" ON "Service"("approvedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_verifiedAt_idx" ON "Service"("verifiedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_spamAt_idx" ON "Service"("spamAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_serviceVisibility_idx" ON "Service"("serviceVisibility");
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userAgent` on the `PushSubscription` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PushSubscription" DROP COLUMN "userAgent";
|
||||||
@@ -353,8 +353,6 @@ model Service {
|
|||||||
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.
|
||||||
isRecentlyListed Boolean @default(false)
|
|
||||||
/// 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)
|
||||||
@@ -363,8 +361,6 @@ model Service {
|
|||||||
verificationSummary String?
|
verificationSummary String?
|
||||||
verificationRequests ServiceVerificationRequest[]
|
verificationRequests ServiceVerificationRequest[]
|
||||||
verificationProofMd String?
|
verificationProofMd String?
|
||||||
/// Computed via trigger when the service status is VERIFICATION_SUCCESS. Do not update through prisma.
|
|
||||||
verifiedAt DateTime?
|
|
||||||
/// [UserSentiment]
|
/// [UserSentiment]
|
||||||
userSentiment Json?
|
userSentiment Json?
|
||||||
userSentimentAt DateTime?
|
userSentimentAt DateTime?
|
||||||
@@ -380,7 +376,16 @@ model Service {
|
|||||||
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.
|
||||||
listedAt DateTime?
|
listedAt DateTime?
|
||||||
|
/// Computed via trigger when the verification status is APPROVED. Do not update through prisma.
|
||||||
|
approvedAt DateTime?
|
||||||
|
/// Computed via trigger when the verification status is VERIFICATION_SUCCESS. Do not update through prisma.
|
||||||
|
verifiedAt DateTime?
|
||||||
|
/// Computed via trigger when the verification status is VERIFICATION_FAILED. Do not update through prisma.
|
||||||
|
spamAt DateTime?
|
||||||
|
/// Computed via trigger. Do not update through prisma.
|
||||||
|
isRecentlyApproved Boolean @default(false)
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
events Event[]
|
events Event[]
|
||||||
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
|
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
|
||||||
@@ -396,6 +401,9 @@ model Service {
|
|||||||
affiliatedUsers ServiceUser[] @relation("ServiceUsers")
|
affiliatedUsers ServiceUser[] @relation("ServiceUsers")
|
||||||
|
|
||||||
@@index([listedAt])
|
@@index([listedAt])
|
||||||
|
@@index([approvedAt])
|
||||||
|
@@index([verifiedAt])
|
||||||
|
@@index([spamAt])
|
||||||
@@index([overallScore])
|
@@index([overallScore])
|
||||||
@@index([privacyScore])
|
@@index([privacyScore])
|
||||||
@@index([trustScore])
|
@@index([trustScore])
|
||||||
@@ -407,6 +415,7 @@ model Service {
|
|||||||
@@index([updatedAt])
|
@@index([updatedAt])
|
||||||
@@index([slug])
|
@@index([slug])
|
||||||
@@index([previousSlugs])
|
@@index([previousSlugs])
|
||||||
|
@@index([serviceVisibility])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ServiceContactMethod {
|
model ServiceContactMethod {
|
||||||
@@ -578,6 +587,7 @@ enum VerificationStepStatus {
|
|||||||
IN_PROGRESS
|
IN_PROGRESS
|
||||||
PASSED
|
PASSED
|
||||||
FAILED
|
FAILED
|
||||||
|
WARNING
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationStep {
|
model VerificationStep {
|
||||||
@@ -677,8 +687,6 @@ model PushSubscription {
|
|||||||
p256dh String
|
p256dh String
|
||||||
/// Authentication secret
|
/// Authentication secret
|
||||||
auth String
|
auth String
|
||||||
/// To identify different devices
|
|
||||||
userAgent String?
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import {
|
|||||||
PrismaClient,
|
PrismaClient,
|
||||||
ServiceSuggestionStatus,
|
ServiceSuggestionStatus,
|
||||||
ServiceUserRole,
|
ServiceUserRole,
|
||||||
VerificationStatus,
|
|
||||||
type Prisma,
|
type Prisma,
|
||||||
type User,
|
type User,
|
||||||
type ServiceVisibility,
|
type ServiceVisibility,
|
||||||
ServiceSuggestionType,
|
ServiceSuggestionType,
|
||||||
KycLevelClarification,
|
KycLevelClarification,
|
||||||
VerificationStepStatus,
|
VerificationStepStatus,
|
||||||
|
type VerificationStatus,
|
||||||
} from '@prisma/client'
|
} from '@prisma/client'
|
||||||
|
import { differenceInDays, isPast } from 'date-fns'
|
||||||
import { omit, uniqBy } from 'lodash-es'
|
import { omit, uniqBy } from 'lodash-es'
|
||||||
import { generateUsername } from 'unique-username-generator'
|
import { generateUsername } from 'unique-username-generator'
|
||||||
|
|
||||||
@@ -614,6 +615,14 @@ const generateFakeService = (users: User[]) => {
|
|||||||
const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), {
|
const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), {
|
||||||
probability: 0.8,
|
probability: 0.8,
|
||||||
})
|
})
|
||||||
|
const serviceVisibility = faker.helpers.weightedArrayElement<ServiceVisibility>([
|
||||||
|
{ weight: 80, value: 'PUBLIC' },
|
||||||
|
{ weight: 10, value: 'UNLISTED' },
|
||||||
|
{ weight: 5, value: 'HIDDEN' },
|
||||||
|
{ weight: 5, value: 'ARCHIVED' },
|
||||||
|
])
|
||||||
|
const approvedAt =
|
||||||
|
status === 'APPROVED' || status === 'VERIFICATION_SUCCESS' ? faker.date.recent({ days: 30 }) : null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -629,12 +638,7 @@ const generateFakeService = (users: User[]) => {
|
|||||||
overallScore: 0,
|
overallScore: 0,
|
||||||
privacyScore: 0,
|
privacyScore: 0,
|
||||||
trustScore: 0,
|
trustScore: 0,
|
||||||
serviceVisibility: faker.helpers.weightedArrayElement<ServiceVisibility>([
|
serviceVisibility,
|
||||||
{ weight: 80, value: 'PUBLIC' },
|
|
||||||
{ weight: 10, value: 'UNLISTED' },
|
|
||||||
{ weight: 5, value: 'HIDDEN' },
|
|
||||||
{ weight: 5, value: 'ARCHIVED' },
|
|
||||||
]),
|
|
||||||
verificationStatus: status,
|
verificationStatus: status,
|
||||||
verificationSummary:
|
verificationSummary:
|
||||||
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null,
|
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null,
|
||||||
@@ -677,8 +681,14 @@ 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`,
|
||||||
listedAt: faker.date.past(),
|
listedAt:
|
||||||
verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null,
|
serviceVisibility === 'PUBLIC' || serviceVisibility === 'ARCHIVED'
|
||||||
|
? faker.date.recent({ days: 30 })
|
||||||
|
: null,
|
||||||
|
verifiedAt: status === 'VERIFICATION_SUCCESS' ? faker.date.recent({ days: 30 }) : null,
|
||||||
|
spamAt: status === 'VERIFICATION_FAILED' ? faker.date.recent({ days: 30 }) : null,
|
||||||
|
approvedAt,
|
||||||
|
isRecentlyApproved: !!approvedAt && isPast(approvedAt) && differenceInDays(new Date(), approvedAt) < 15,
|
||||||
tosReview,
|
tosReview,
|
||||||
tosReviewAt: tosReview
|
tosReviewAt: tosReview
|
||||||
? faker.date.recent()
|
? faker.date.recent()
|
||||||
@@ -908,7 +918,7 @@ const generateFakeServiceContactMethod = (serviceId: number) => {
|
|||||||
value: `https://linkedin.com/in/${faker.helpers.slugify(faker.person.fullName())}`,
|
value: `https://linkedin.com/in/${faker.helpers.slugify(faker.person.fullName())}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: faker.lorem.word({ length: 2 }),
|
label: 'Custom label',
|
||||||
value: `https://bitcointalk.org/index.php?topic=${faker.number.int({ min: 1, max: 1000000 }).toString()}.0`,
|
value: `https://bitcointalk.org/index.php?topic=${faker.number.int({ min: 1, max: 1000000 }).toString()}.0`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -918,7 +928,7 @@ const generateFakeServiceContactMethod = (serviceId: number) => {
|
|||||||
value: faker.internet.url(),
|
value: faker.internet.url(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: faker.lorem.word({ length: 2 }),
|
label: 'Custom label',
|
||||||
value: faker.internet.url(),
|
value: faker.internet.url(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1307,7 +1317,7 @@ async function main() {
|
|||||||
const service = await prisma.service.create({
|
const service = await prisma.service.create({
|
||||||
data: {
|
data: {
|
||||||
...serviceData,
|
...serviceData,
|
||||||
verificationStatus: VerificationStatus.COMMUNITY_CONTRIBUTED,
|
verificationStatus: 'COMMUNITY_CONTRIBUTED',
|
||||||
categories: {
|
categories: {
|
||||||
connect: randomCategories.map((cat) => ({ id: cat.id })),
|
connect: randomCategories.map((cat) => ({ id: cat.id })),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- This script defines PostgreSQL functions and triggers for managing service scores:
|
-- This script defines PostgreSQL functions and triggers for managing service scores:
|
||||||
-- 1. Automatically calculates and updates privacy, trust, and overall scores
|
-- 1. Automatically calculates and updates privacy, trust, and overall scores
|
||||||
-- for services when services or their attributes change.
|
-- for services when services or their attributes change.
|
||||||
-- 2. Updates the isRecentlyListed flag for services listed within the last 15 days.
|
-- 2. Updates the isRecentlyApproved flag for services approved within the last 15 days.
|
||||||
-- 3. Queues asynchronous score recalculation in "ServiceScoreRecalculationJob"
|
-- 3. Queues asynchronous score recalculation in "ServiceScoreRecalculationJob"
|
||||||
-- when an "Attribute" definition (e.g., points) is updated, ensuring
|
-- when an "Attribute" definition (e.g., points) is updated, ensuring
|
||||||
-- efficient handling of widespread score updates.
|
-- efficient handling of widespread score updates.
|
||||||
@@ -24,12 +24,9 @@ RETURNS INT AS $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
privacy_score INT := 0;
|
privacy_score INT := 0;
|
||||||
kyc_factor INT;
|
kyc_factor INT;
|
||||||
onion_factor INT := 0;
|
clarification_factor INT := 0;
|
||||||
i2p_factor INT := 0;
|
onion_or_i2p_factor INT := 0;
|
||||||
monero_factor INT := 0;
|
monero_factor INT := 0;
|
||||||
open_source_factor INT := 0;
|
|
||||||
p2p_factor INT := 0;
|
|
||||||
decentralized_factor INT := 0;
|
|
||||||
attributes_score INT := 0;
|
attributes_score INT := 0;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get service data
|
-- Get service data
|
||||||
@@ -46,20 +43,22 @@ BEGIN
|
|||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE "id" = service_id;
|
WHERE "id" = service_id;
|
||||||
|
|
||||||
-- Check for onion URLs
|
-- Adjust score based on KYC level clarification modifiers
|
||||||
IF EXISTS (
|
SELECT
|
||||||
SELECT 1 FROM "Service"
|
CASE
|
||||||
WHERE "id" = service_id AND array_length("onionUrls", 1) > 0
|
WHEN "kycLevelClarification" = 'DEPENDS_ON_PARTNERS' THEN -5
|
||||||
) THEN
|
ELSE 0 -- Default modifier when no clarification or unrecognized value
|
||||||
onion_factor := 5;
|
END
|
||||||
END IF;
|
INTO clarification_factor
|
||||||
|
FROM "Service"
|
||||||
|
WHERE "id" = service_id;
|
||||||
|
|
||||||
-- Check for i2p URLs
|
-- Check for onion or i2p URLs
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM "Service"
|
SELECT 1 FROM "Service"
|
||||||
WHERE "id" = service_id AND array_length("i2pUrls", 1) > 0
|
WHERE "id" = service_id AND (array_length("onionUrls", 1) > 0 OR array_length("i2pUrls", 1) > 0)
|
||||||
) THEN
|
) THEN
|
||||||
i2p_factor := 5;
|
onion_or_i2p_factor := 5;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Check for Monero acceptance
|
-- Check for Monero acceptance
|
||||||
@@ -75,10 +74,10 @@ BEGIN
|
|||||||
INTO attributes_score
|
INTO attributes_score
|
||||||
FROM "ServiceAttribute" sa
|
FROM "ServiceAttribute" sa
|
||||||
JOIN "Attribute" a ON sa."attributeId" = a."id"
|
JOIN "Attribute" a ON sa."attributeId" = a."id"
|
||||||
WHERE sa."serviceId" = service_id AND a."category" = 'PRIVACY';
|
WHERE sa."serviceId" = service_id;
|
||||||
|
|
||||||
-- Calculate final privacy score (base 100)
|
-- Calculate final privacy score (base 100)
|
||||||
privacy_score := 50 + kyc_factor + onion_factor + i2p_factor + monero_factor + open_source_factor + p2p_factor + decentralized_factor + attributes_score;
|
privacy_score := 50 + kyc_factor + clarification_factor + onion_or_i2p_factor + monero_factor + attributes_score;
|
||||||
|
|
||||||
-- Ensure the score is in reasonable bounds (0-100)
|
-- Ensure the score is in reasonable bounds (0-100)
|
||||||
privacy_score := GREATEST(0, LEAST(100, privacy_score));
|
privacy_score := GREATEST(0, LEAST(100, privacy_score));
|
||||||
@@ -94,7 +93,7 @@ DECLARE
|
|||||||
trust_score INT := 0;
|
trust_score INT := 0;
|
||||||
verification_factor INT;
|
verification_factor INT;
|
||||||
attributes_score INT := 0;
|
attributes_score INT := 0;
|
||||||
recently_listed_factor INT := 0;
|
recently_approved_factor INT := 0;
|
||||||
tos_penalty_factor INT := 0;
|
tos_penalty_factor INT := 0;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get verification status factor
|
-- Get verification status factor
|
||||||
@@ -115,26 +114,26 @@ BEGIN
|
|||||||
INTO attributes_score
|
INTO attributes_score
|
||||||
FROM "ServiceAttribute" sa
|
FROM "ServiceAttribute" sa
|
||||||
JOIN "Attribute" a ON sa."attributeId" = a.id
|
JOIN "Attribute" a ON sa."attributeId" = a.id
|
||||||
WHERE sa."serviceId" = service_id AND a.category = 'TRUST';
|
WHERE sa."serviceId" = service_id;
|
||||||
|
|
||||||
-- Apply penalty if service was listed within the last 15 days
|
-- Apply penalty if service was approved within the last 15 days
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE id = service_id
|
WHERE id = service_id
|
||||||
AND "listedAt" IS NOT NULL
|
AND "approvedAt" IS NOT NULL
|
||||||
AND "verificationStatus" = 'APPROVED'
|
AND "verificationStatus" = 'APPROVED'
|
||||||
AND (NOW() - "listedAt") <= INTERVAL '15 days'
|
AND (NOW() - "approvedAt") <= INTERVAL '15 days'
|
||||||
) THEN
|
) THEN
|
||||||
recently_listed_factor := -10;
|
recently_approved_factor := -10;
|
||||||
-- Update the isRecentlyListed flag to true
|
-- Update the isRecentlyApproved flag to true
|
||||||
UPDATE "Service"
|
UPDATE "Service"
|
||||||
SET "isRecentlyListed" = TRUE
|
SET "isRecentlyApproved" = TRUE
|
||||||
WHERE id = service_id;
|
WHERE id = service_id;
|
||||||
ELSE
|
ELSE
|
||||||
-- Update the isRecentlyListed flag to false
|
-- Update the isRecentlyApproved flag to false
|
||||||
UPDATE "Service"
|
UPDATE "Service"
|
||||||
SET "isRecentlyListed" = FALSE
|
SET "isRecentlyApproved" = FALSE
|
||||||
WHERE id = service_id;
|
WHERE id = service_id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Calculate final trust score (base 100)
|
-- Calculate final trust score (base 100)
|
||||||
trust_score := 50 + verification_factor + attributes_score + recently_listed_factor + tos_penalty_factor;
|
trust_score := 50 + verification_factor + attributes_score + recently_approved_factor + tos_penalty_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));
|
||||||
@@ -165,7 +164,7 @@ RETURNS INT AS $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
overall_score INT;
|
overall_score INT;
|
||||||
BEGIN
|
BEGIN
|
||||||
overall_score := CAST(ROUND(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0) AS INT);
|
overall_score := CAST(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0 AS INT);
|
||||||
RETURN GREATEST(0, LEAST(10, overall_score));
|
RETURN GREATEST(0, LEAST(10, overall_score));
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|||||||
@@ -1,48 +1,60 @@
|
|||||||
-- This script manages the `listedAt`, `verifiedAt`, and `isRecentlyListed` timestamps
|
CREATE OR REPLACE FUNCTION manage_service_visibility_timestamps()
|
||||||
-- for services based on changes to their `verificationStatus`. It ensures these timestamps
|
|
||||||
-- are set or cleared appropriately when a service's verification status is updated.
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION manage_service_timestamps()
|
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Manage listedAt timestamp
|
IF NEW."serviceVisibility" = 'PUBLIC' OR NEW."serviceVisibility" = 'ARCHIVED' THEN
|
||||||
IF NEW."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN
|
|
||||||
-- Set listedAt only on the first time status becomes APPROVED or VERIFICATION_SUCCESS
|
|
||||||
IF OLD."listedAt" IS NULL THEN
|
IF OLD."listedAt" IS NULL THEN
|
||||||
NEW."listedAt" := NOW();
|
NEW."listedAt" := NOW();
|
||||||
NEW."isRecentlyListed" := TRUE;
|
|
||||||
END IF;
|
END IF;
|
||||||
ELSIF OLD."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN
|
ELSE
|
||||||
-- Clear listedAt if the status changes FROM APPROVED or VERIFICATION_SUCCESS to something else
|
|
||||||
-- The trigger's WHEN clause ensures NEW."verificationStatus" is different.
|
|
||||||
NEW."listedAt" := NULL;
|
NEW."listedAt" := NULL;
|
||||||
NEW."isRecentlyListed" := FALSE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Manage verifiedAt timestamp
|
|
||||||
IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
|
|
||||||
-- Set verifiedAt when status changes TO VERIFICATION_SUCCESS
|
|
||||||
NEW."verifiedAt" := NOW();
|
|
||||||
NEW."isRecentlyListed" := FALSE;
|
|
||||||
ELSIF OLD."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
|
|
||||||
-- Clear verifiedAt when status changes FROM VERIFICATION_SUCCESS
|
|
||||||
-- The trigger's WHEN clause ensures NEW."verificationStatus" is different.
|
|
||||||
NEW."verifiedAt" := NULL;
|
|
||||||
NEW."isRecentlyListed" := FALSE;
|
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- Drop the old trigger first if it exists under the old name
|
CREATE OR REPLACE FUNCTION manage_service_verification_timestamps()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (NEW."verificationStatus" = 'APPROVED' OR NEW."verificationStatus" = 'VERIFICATION_SUCCESS') THEN
|
||||||
|
IF OLD."approvedAt" IS NULL THEN
|
||||||
|
NEW."approvedAt" := NOW();
|
||||||
|
NEW."isRecentlyApproved" := TRUE;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
NEW."approvedAt" := NULL;
|
||||||
|
NEW."isRecentlyApproved" := FALSE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
|
||||||
|
NEW."verifiedAt" := NOW();
|
||||||
|
ELSE
|
||||||
|
NEW."verifiedAt" := NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW."verificationStatus" = 'VERIFICATION_FAILED' THEN
|
||||||
|
NEW."spamAt" := NOW();
|
||||||
|
ELSE
|
||||||
|
NEW."spamAt" := NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Drop the old triggers TODO: remove this some day
|
||||||
DROP TRIGGER IF EXISTS trigger_set_service_listed_at ON "Service";
|
DROP TRIGGER IF EXISTS trigger_set_service_listed_at ON "Service";
|
||||||
-- Drop the trigger if it exists under the new name
|
|
||||||
DROP TRIGGER IF EXISTS trigger_manage_service_timestamps ON "Service";
|
DROP TRIGGER IF EXISTS trigger_manage_service_timestamps ON "Service";
|
||||||
|
|
||||||
CREATE TRIGGER trigger_manage_service_timestamps
|
DROP TRIGGER IF EXISTS trigger_manage_service_visibility_timestamps ON "Service";
|
||||||
|
DROP TRIGGER IF EXISTS trigger_manage_service_verification_timestamps ON "Service";
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_manage_service_visibility_timestamps
|
||||||
|
BEFORE UPDATE OF "serviceVisibility" ON "Service"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION manage_service_visibility_timestamps();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_manage_service_verification_timestamps
|
||||||
BEFORE UPDATE OF "verificationStatus" ON "Service"
|
BEFORE UPDATE OF "verificationStatus" ON "Service"
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
-- Only execute the function if the verificationStatus value has actually changed
|
EXECUTE FUNCTION manage_service_verification_timestamps();
|
||||||
WHEN (OLD."verificationStatus" IS DISTINCT FROM NEW."verificationStatus")
|
|
||||||
EXECUTE FUNCTION manage_service_timestamps();
|
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ export const adminServiceActions = {
|
|||||||
verificationSummary: input.verificationSummary,
|
verificationSummary: input.verificationSummary,
|
||||||
verificationProofMd: input.verificationProofMd,
|
verificationProofMd: input.verificationProofMd,
|
||||||
acceptedCurrencies: input.acceptedCurrencies,
|
acceptedCurrencies: input.acceptedCurrencies,
|
||||||
referral: input.referral,
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
referral: input.referral || null,
|
||||||
serviceVisibility: input.serviceVisibility,
|
serviceVisibility: input.serviceVisibility,
|
||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
overallScore: input.overallScore,
|
overallScore: input.overallScore,
|
||||||
@@ -244,7 +245,8 @@ export const adminServiceActions = {
|
|||||||
verificationSummary: input.verificationSummary,
|
verificationSummary: input.verificationSummary,
|
||||||
verificationProofMd: input.verificationProofMd,
|
verificationProofMd: input.verificationProofMd,
|
||||||
acceptedCurrencies: input.acceptedCurrencies,
|
acceptedCurrencies: input.acceptedCurrencies,
|
||||||
referral: input.referral,
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
referral: input.referral || null,
|
||||||
serviceVisibility: input.serviceVisibility,
|
serviceVisibility: input.serviceVisibility,
|
||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
overallScore: input.overallScore,
|
overallScore: input.overallScore,
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ export const apiServiceActions = {
|
|||||||
tosUrls: true,
|
tosUrls: true,
|
||||||
referral: true,
|
referral: true,
|
||||||
listedAt: true,
|
listedAt: true,
|
||||||
|
approvedAt: true,
|
||||||
verifiedAt: true,
|
verifiedAt: true,
|
||||||
serviceVisibility: true,
|
serviceVisibility: true,
|
||||||
} as const satisfies Prisma.ServiceSelect
|
} as const satisfies Prisma.ServiceSelect
|
||||||
|
|
||||||
let service = await prisma.service.findFirst({
|
let service = await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
|
|
||||||
OR: [
|
OR: [
|
||||||
@@ -92,7 +92,6 @@ export const apiServiceActions = {
|
|||||||
if (!service && input.slug) {
|
if (!service && input.slug) {
|
||||||
service = await prisma.service.findFirst({
|
service = await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
|
|
||||||
previousSlugs: { has: input.slug },
|
previousSlugs: { has: input.slug },
|
||||||
@@ -105,9 +104,7 @@ export const apiServiceActions = {
|
|||||||
!service ||
|
!service ||
|
||||||
(service.serviceVisibility !== 'PUBLIC' &&
|
(service.serviceVisibility !== 'PUBLIC' &&
|
||||||
service.serviceVisibility !== 'ARCHIVED' &&
|
service.serviceVisibility !== 'ARCHIVED' &&
|
||||||
service.serviceVisibility !== 'UNLISTED') ||
|
service.serviceVisibility !== 'UNLISTED')
|
||||||
!service.listedAt ||
|
|
||||||
service.listedAt > new Date()
|
|
||||||
) {
|
) {
|
||||||
throw new ActionError({
|
throw new ActionError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
@@ -130,6 +127,7 @@ export const apiServiceActions = {
|
|||||||
'description',
|
'description',
|
||||||
]),
|
]),
|
||||||
verifiedAt: service.verifiedAt,
|
verifiedAt: service.verifiedAt,
|
||||||
|
approvedAt: service.approvedAt,
|
||||||
kycLevel: service.kycLevel,
|
kycLevel: service.kycLevel,
|
||||||
kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']),
|
kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']),
|
||||||
kycLevelClarification: service.kycLevelClarification,
|
kycLevelClarification: service.kycLevelClarification,
|
||||||
|
|||||||
@@ -270,6 +270,18 @@ export const commentActions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRelatedToService = !!(await tx.serviceUser.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_serviceId: {
|
||||||
|
userId: context.locals.user.id,
|
||||||
|
serviceId: input.serviceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
// Prepare data object with proper type safety
|
// Prepare data object with proper type safety
|
||||||
const commentData: Prisma.CommentCreateInput = {
|
const commentData: Prisma.CommentCreateInput = {
|
||||||
content: input.content,
|
content: input.content,
|
||||||
@@ -277,7 +289,12 @@ export const commentActions = {
|
|||||||
author: { connect: { id: context.locals.user.id } },
|
author: { connect: { id: context.locals.user.id } },
|
||||||
|
|
||||||
// Change status to HUMAN_PENDING if there's an issue report, this is so that the AI worker does not pick it up for review
|
// Change status to HUMAN_PENDING if there's an issue report, this is so that the AI worker does not pick it up for review
|
||||||
status: context.locals.user.admin ? 'APPROVED' : isIssueReport ? 'HUMAN_PENDING' : 'PENDING',
|
status:
|
||||||
|
context.locals.user.admin || context.locals.user.moderator || isRelatedToService
|
||||||
|
? 'APPROVED'
|
||||||
|
: isIssueReport
|
||||||
|
? 'HUMAN_PENDING'
|
||||||
|
: 'PENDING',
|
||||||
requiresAdminReview,
|
requiresAdminReview,
|
||||||
orderId: input.orderId?.trim() ?? null,
|
orderId: input.orderId?.trim() ?? null,
|
||||||
kycRequested: input.issueKycRequested === true,
|
kycRequested: input.issueKycRequested === true,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const notificationActions = {
|
|||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
p256dhKey: z.string(),
|
p256dhKey: z.string(),
|
||||||
authKey: z.string(),
|
authKey: z.string(),
|
||||||
userAgent: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
handler: async (input, context) => {
|
handler: async (input, context) => {
|
||||||
await prisma.pushSubscription.upsert({
|
await prisma.pushSubscription.upsert({
|
||||||
@@ -43,14 +42,12 @@ export const notificationActions = {
|
|||||||
update: {
|
update: {
|
||||||
p256dh: input.p256dhKey,
|
p256dh: input.p256dhKey,
|
||||||
auth: input.authKey,
|
auth: input.authKey,
|
||||||
userAgent: input.userAgent,
|
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: context.locals.user.id,
|
userId: context.locals.user.id,
|
||||||
endpoint: input.endpoint,
|
endpoint: input.endpoint,
|
||||||
p256dh: input.p256dhKey,
|
p256dh: input.p256dhKey,
|
||||||
auth: input.authKey,
|
auth: input.authKey,
|
||||||
userAgent: input.userAgent,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -58,25 +55,17 @@ export const notificationActions = {
|
|||||||
|
|
||||||
unsubscribe: defineProtectedAction({
|
unsubscribe: defineProtectedAction({
|
||||||
accept: 'json',
|
accept: 'json',
|
||||||
permissions: 'user',
|
permissions: 'guest',
|
||||||
input: z.object({
|
input: z.object({
|
||||||
endpoint: z.string().optional(),
|
endpoint: z.string(),
|
||||||
}),
|
}),
|
||||||
handler: async (input, context) => {
|
handler: async (input, context) => {
|
||||||
if (input.endpoint) {
|
await prisma.pushSubscription.delete({
|
||||||
await prisma.pushSubscription.deleteMany({
|
where: {
|
||||||
where: {
|
userId: context.locals.user?.id ?? undefined,
|
||||||
userId: context.locals.user.id,
|
endpoint: input.endpoint,
|
||||||
endpoint: input.endpoint,
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await prisma.pushSubscription.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: context.locals.user.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const findPossibleDuplicates = async (input: { name: string }) => {
|
|||||||
id: {
|
id: {
|
||||||
in: matches.map(({ id }) => id),
|
in: matches.map(({ id }) => id),
|
||||||
},
|
},
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: {
|
serviceVisibility: {
|
||||||
in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'],
|
in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'],
|
||||||
},
|
},
|
||||||
@@ -252,7 +251,6 @@ export const serviceSuggestionActions = {
|
|||||||
overallScore: 0,
|
overallScore: 0,
|
||||||
privacyScore: 0,
|
privacyScore: 0,
|
||||||
trustScore: 0,
|
trustScore: 0,
|
||||||
listedAt: new Date(),
|
|
||||||
serviceVisibility: 'UNLISTED',
|
serviceVisibility: 'UNLISTED',
|
||||||
categories: {
|
categories: {
|
||||||
connect: input.categories.map((id) => ({ id })),
|
connect: input.categories.map((id) => ({ id })),
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
8
web/src/assets/review-badge/long-black.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="32" width="216" viewBox="0 0 432 64">
|
||||||
|
<rect width="431" height="63" x=".5" y=".5" fill="#101413" stroke="#292B2A" rx="7.5" />
|
||||||
|
<path fill="#3BDB78" d="m37.5 18 4.1 8.3 9.2 1.4-6.6 6.5 1.5 9.1-8.2-4.3-8.2 4.3 1.5-9.1-6.6-6.5 9.2-1.4 4.1-8.3Z" />
|
||||||
|
<path fill="#BEBEBE"
|
||||||
|
d="M63.7 42V22.4h7c1.5 0 2.7.2 3.7.7 1 .6 1.8 1.3 2.3 2.2.5 1 .8 2 .8 3.2 0 1.2-.3 2.3-.8 3.2-.5.9-1.3 1.6-2.3 2.1-1 .5-2.2.8-3.8.8h-5.3V32h5c1 0 1.8-.1 2.4-.4.6-.3 1-.7 1.4-1.2a4 4 0 0 0 .4-1.9 4 4 0 0 0-.5-2c-.2-.5-.7-.9-1.3-1.2-.6-.2-1.4-.4-2.4-.4h-3.7V42h-3Zm9.7-8.9 4.8 8.9h-3.4l-4.7-8.9h3.3ZM87 42.3c-1.5 0-2.7-.3-3.8-1-1-.6-1.8-1.4-2.4-2.6a9 9 0 0 1-.8-4 9 9 0 0 1 .8-4c.6-1.1 1.4-2 2.4-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.3 1 .5 2 .5 3.4v1H81.6v-2.1h8.9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.8 0-1.5.2-2.1.6a4 4 0 0 0-1.4 1.6c-.3.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.6 2.5.3.7.8 1.2 1.4 1.6.7.4 1.4.5 2.2.5.6 0 1 0 1.5-.2l1.2-.7c.3-.3.5-.7.7-1.2l2.7.5a5 5 0 0 1-1.1 2.1c-.6.6-1.3 1-2.1 1.4-.9.3-1.8.5-3 .5Zm21.7-15L103.3 42h-3l-5.4-14.7h3l3.8 11.3h.2l3.7-11.3h3Zm2.7 14.7V27.3h2.8V42h-2.8Zm1.4-17c-.5 0-1-.2-1.3-.5-.3-.3-.5-.7-.5-1.2s.2-.9.5-1.2c.4-.4.8-.5 1.3-.5s1 .1 1.3.5c.3.3.5.7.5 1.2s-.2.9-.5 1.2c-.4.3-.8.5-1.3.5Zm11.6 17.3c-1.4 0-2.7-.3-3.7-1-1-.6-1.9-1.4-2.4-2.6a9 9 0 0 1-.9-4 9 9 0 0 1 .9-4c.5-1.1 1.3-2 2.3-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.4 1 .6 2 .6 3.4v1H119v-2.1h9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.9 0-1.6.2-2.2.6a4 4 0 0 0-1.3 1.6c-.4.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.5 2.5.4.7.8 1.2 1.5 1.6.6.4 1.3.5 2.2.5.5 0 1 0 1.4-.2.5-.2.9-.4 1.2-.7.3-.3.6-.7.8-1.2l2.7.5a5 5 0 0 1-1.2 2.1c-.6.6-1.3 1-2.1 1.4-.8.3-1.8.5-2.9.5Zm12.7-.3-4.3-14.7h3l2.8 10.8h.2l2.9-10.8h3l2.8 10.7h.1l2.9-10.7h3L149 42h-2.9l-3-10.6h-.2L140 42h-2.9Zm32.4.3c-1.3 0-2.6-.3-3.6-1-1-.6-1.8-1.5-2.4-2.6a8.8 8.8 0 0 1-.8-4c0-1.5.3-2.9.8-4a6.4 6.4 0 0 1 6-3.6c1.4 0 2.6.3 3.7 1 1 .6 1.8 1.5 2.3 2.6.6 1.1.9 2.5.9 4s-.3 2.9-.9 4a6.4 6.4 0 0 1-6 3.6Zm0-2.4c1 0 1.7-.2 2.3-.7.6-.5 1-1.1 1.3-2 .3-.7.4-1.6.4-2.5 0-1-.1-1.8-.4-2.6-.3-.8-.7-1.4-1.3-1.9-.6-.5-1.4-.7-2.3-.7-.9 0-1.6.2-2.2.7-.6.5-1 1.1-1.3 2-.3.7-.4 1.6-.4 2.5 0 1 .1 1.8.4 2.6.3.8.7 1.4 1.3 1.9.6.5 1.3.7 2.2.7Zm13-6.6V42h-2.9V27.3h2.8v2.4h.1c.4-.8.9-1.4 1.6-2a5 5 0 0 1 2.8-.6c1 0 1.9.2 2.6.6.8.4 1.4 1 1.8 1.9.4.8.6 1.8.6 3V42H189v-9c0-1-.3-2-.8-2.5a3 3 0 0 0-2.3-1c-.7 0-1.3.2-1.8.5s-.9.7-1.2 1.3c-.3.5-.4 1.2-.4 2Z" />
|
||||||
|
<path fill="#3BDB78"
|
||||||
|
d="M205.5 18a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1h-74Zm4 4h2a1 1 0 0 1 1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-3h-7a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V23a1 1 0 0 1 1-1Zm12 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm12.8 0h2.4a1 1 0 0 1 .8.5l5 7.8 5-7.8a1 1 0 0 1 .8-.5h2.4a1 1 0 0 1 .8 1.5l-6.8 10.8a1 1 0 0 0-.2.6V41a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6.1c0-.2 0-.4-.2-.6l-6.8-10.8a1 1 0 0 1 .3-1.4l.5-.1Zm27.2 0h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-15v12h15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1V27a1 1 0 0 1 1-1h3v-3a1 1 0 0 1 1-1Zm24 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V25.6l9.2 15.9c.2.3.5.5.8.5h5a1 1 0 0 0 1-1V23a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v15.4l-9.2-15.9a1 1 0 0 0-.8-.5h-5Zm29 0a1 1 0 0 0-1 1v3h12v-3a1 1 0 0 0-1-1h-10Zm11 4v12h3a1 1 0 0 0 1-1V27a1 1 0 0 0-1-1h-3Zm0 12h-12v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3Zm-12 0V26h-3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3Zm21-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h4v15a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V26h4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-18Zm27 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V29.4l5.5 12a1 1 0 0 0 1 .6h3a1 1 0 0 0 1-.6l5.5-12V41a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V23a1 1 0 0 0-1-1h-3.4a1 1 0 0 0-.9.6l-6.7 14.6-6.7-14.6a1 1 0 0 0-1-.6h-3.3Zm32 0a1 1 0 0 0-1 1v3h15a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14Zm-1 4h-3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14a1 1 0 0 1-1-1v-3h7a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-7v-4Zm-38 12a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
8
web/src/assets/review-badge/long-white.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="32" width="216" viewBox="0 0 432 64">
|
||||||
|
<rect width="431" height="63" x=".5" y=".5" fill="#fff" stroke="#ECF0EE" rx="7.5" />
|
||||||
|
<path fill="#28AE5B" d="m37.5 18 4.1 8.3 9.2 1.4-6.6 6.5 1.5 9.1-8.2-4.3-8.2 4.3 1.5-9.1-6.6-6.5 9.2-1.4 4.1-8.3Z" />
|
||||||
|
<path fill="#3F3F3F"
|
||||||
|
d="M63.7 42V22.4h7c1.5 0 2.7.2 3.7.7 1 .6 1.8 1.3 2.3 2.2.5 1 .8 2 .8 3.2 0 1.2-.3 2.3-.8 3.2-.5.9-1.3 1.6-2.3 2.1-1 .5-2.2.8-3.8.8h-5.3V32h5c1 0 1.8-.1 2.4-.4.6-.3 1-.7 1.4-1.2a4 4 0 0 0 .4-1.9 4 4 0 0 0-.5-2c-.2-.5-.7-.9-1.3-1.2-.6-.2-1.4-.4-2.4-.4h-3.7V42h-3Zm9.7-8.9 4.8 8.9h-3.4l-4.7-8.9h3.3ZM87 42.3c-1.5 0-2.7-.3-3.8-1-1-.6-1.8-1.4-2.4-2.6a9 9 0 0 1-.8-4 9 9 0 0 1 .8-4c.6-1.1 1.4-2 2.4-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.3 1 .5 2 .5 3.4v1H81.6v-2.1h8.9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.8 0-1.5.2-2.1.6a4 4 0 0 0-1.4 1.6c-.3.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.6 2.5.3.7.8 1.2 1.4 1.6.7.4 1.4.5 2.2.5.6 0 1 0 1.5-.2l1.2-.7c.3-.3.5-.7.7-1.2l2.7.5a5 5 0 0 1-1.1 2.1c-.6.6-1.3 1-2.1 1.4-.9.3-1.8.5-3 .5Zm21.7-15L103.3 42h-3l-5.4-14.7h3l3.8 11.3h.2l3.7-11.3h3Zm2.7 14.7V27.3h2.8V42h-2.8Zm1.4-17c-.5 0-1-.2-1.3-.5-.3-.3-.5-.7-.5-1.2s.2-.9.5-1.2c.4-.4.8-.5 1.3-.5s1 .1 1.3.5c.3.3.5.7.5 1.2s-.2.9-.5 1.2c-.4.3-.8.5-1.3.5Zm11.6 17.3c-1.4 0-2.7-.3-3.7-1-1-.6-1.9-1.4-2.4-2.6a9 9 0 0 1-.9-4 9 9 0 0 1 .9-4c.5-1.1 1.3-2 2.3-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.4 1 .6 2 .6 3.4v1H119v-2.1h9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.9 0-1.6.2-2.2.6a4 4 0 0 0-1.3 1.6c-.4.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.5 2.5.4.7.8 1.2 1.5 1.6.6.4 1.3.5 2.2.5.5 0 1 0 1.4-.2.5-.2.9-.4 1.2-.7.3-.3.6-.7.8-1.2l2.7.5a5 5 0 0 1-1.2 2.1c-.6.6-1.3 1-2.1 1.4-.8.3-1.8.5-2.9.5Zm12.7-.3-4.3-14.7h3l2.8 10.8h.2l2.9-10.8h3l2.8 10.7h.1l2.9-10.7h3L149 42h-2.9l-3-10.6h-.2L140 42h-2.9Zm32.4.3c-1.3 0-2.6-.3-3.6-1-1-.6-1.8-1.5-2.4-2.6a8.8 8.8 0 0 1-.8-4c0-1.5.3-2.9.8-4a6.4 6.4 0 0 1 6-3.6c1.4 0 2.6.3 3.7 1 1 .6 1.8 1.5 2.3 2.6.6 1.1.9 2.5.9 4s-.3 2.9-.9 4a6.4 6.4 0 0 1-6 3.6Zm0-2.4c1 0 1.7-.2 2.3-.7.6-.5 1-1.1 1.3-2 .3-.7.4-1.6.4-2.5 0-1-.1-1.8-.4-2.6-.3-.8-.7-1.4-1.3-1.9-.6-.5-1.4-.7-2.3-.7-.9 0-1.6.2-2.2.7-.6.5-1 1.1-1.3 2-.3.7-.4 1.6-.4 2.5 0 1 .1 1.8.4 2.6.3.8.7 1.4 1.3 1.9.6.5 1.3.7 2.2.7Zm13-6.6V42h-2.9V27.3h2.8v2.4h.1c.4-.8.9-1.4 1.6-2a5 5 0 0 1 2.8-.6c1 0 1.9.2 2.6.6.8.4 1.4 1 1.8 1.9.4.8.6 1.8.6 3V42H189v-9c0-1-.3-2-.8-2.5a3 3 0 0 0-2.3-1c-.7 0-1.3.2-1.8.5s-.9.7-1.2 1.3c-.3.5-.4 1.2-.4 2Z" />
|
||||||
|
<path fill="#28AE5B"
|
||||||
|
d="M205.5 18a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1h-74Zm4 4h2a1 1 0 0 1 1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-3h-7a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V23a1 1 0 0 1 1-1Zm12 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm12.8 0h2.4a1 1 0 0 1 .8.5l5 7.8 5-7.8a1 1 0 0 1 .8-.5h2.4a1 1 0 0 1 .8 1.5l-6.8 10.8a1 1 0 0 0-.2.6V41a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6.1c0-.2 0-.4-.2-.6l-6.8-10.8a1 1 0 0 1 .3-1.4l.5-.1Zm27.2 0h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-15v12h15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1V27a1 1 0 0 1 1-1h3v-3a1 1 0 0 1 1-1Zm24 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V25.6l9.2 15.9c.2.3.5.5.8.5h5a1 1 0 0 0 1-1V23a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v15.4l-9.2-15.9a1 1 0 0 0-.8-.5h-5Zm29 0a1 1 0 0 0-1 1v3h12v-3a1 1 0 0 0-1-1h-10Zm11 4v12h3a1 1 0 0 0 1-1V27a1 1 0 0 0-1-1h-3Zm0 12h-12v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3Zm-12 0V26h-3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3Zm21-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h4v15a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V26h4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-18Zm27 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V29.4l5.5 12a1 1 0 0 0 1 .6h3a1 1 0 0 0 1-.6l5.5-12V41a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V23a1 1 0 0 0-1-1h-3.4a1 1 0 0 0-.9.6l-6.7 14.6-6.7-14.6a1 1 0 0 0-1-.6h-3.3Zm32 0a1 1 0 0 0-1 1v3h15a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14Zm-1 4h-3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14a1 1 0 0 1-1-1v-3h7a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-7v-4Zm-38 12a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
8
web/src/assets/review-badge/short-black.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="48" width="128" viewBox="0 0 256 96">
|
||||||
|
<rect width="255" height="95" x=".5" y=".5" fill="#101413" stroke="#292B2A" rx="7.5" />
|
||||||
|
<path fill="#3BDB78" d="m56.5 16 4.1 8.3 9.2 1.4-6.6 6.5 1.5 9.1-8.2-4.3-8.2 4.3 1.5-9.1-6.6-6.5 9.2-1.4 4.1-8.3Z" />
|
||||||
|
<path fill="#BEBEBE"
|
||||||
|
d="M82.7 40V20.4h7c1.5 0 2.7.2 3.7.7 1 .6 1.8 1.3 2.3 2.2.5 1 .8 2 .8 3.2 0 1.2-.3 2.3-.8 3.2-.5.9-1.3 1.6-2.3 2.1-1 .5-2.2.8-3.8.8h-5.3V30h5c1 0 1.8-.1 2.4-.4.6-.3 1-.7 1.4-1.2a4 4 0 0 0 .4-1.9 4 4 0 0 0-.5-2c-.2-.5-.7-.9-1.3-1.2-.6-.2-1.4-.4-2.4-.4h-3.7V40h-3Zm9.7-8.9 4.8 8.9h-3.4l-4.7-8.9h3.3Zm13.6 9.2c-1.5 0-2.7-.3-3.8-1-1-.6-1.8-1.4-2.4-2.6a9 9 0 0 1-.8-4 9 9 0 0 1 .8-4c.6-1.1 1.4-2 2.4-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.3 1 .5 2 .5 3.4v1h-11.7v-2.1h8.9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.8 0-1.5.2-2.1.6a4 4 0 0 0-1.4 1.6c-.3.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.6 2.5.3.7.8 1.2 1.4 1.6.7.4 1.4.5 2.2.5.6 0 1 0 1.5-.2l1.2-.7c.3-.3.5-.7.7-1.2l2.7.5a5 5 0 0 1-1.1 2.1c-.6.6-1.3 1-2.1 1.4-.9.3-1.8.5-3 .5Zm21.7-15L122.3 40h-3l-5.4-14.7h3l3.8 11.3h.2l3.7-11.3h3Zm2.7 14.7V25.3h2.8V40h-2.8Zm1.4-17c-.5 0-1-.2-1.3-.5-.3-.3-.5-.7-.5-1.2s.2-.9.5-1.2c.4-.4.8-.5 1.3-.5s1 .1 1.3.5c.3.3.5.7.5 1.2s-.2.9-.5 1.2c-.4.3-.8.5-1.3.5Zm11.6 17.3c-1.4 0-2.7-.3-3.7-1-1-.6-1.9-1.4-2.4-2.6a9 9 0 0 1-.9-4 9 9 0 0 1 .9-4c.5-1.1 1.3-2 2.3-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.4 1 .6 2 .6 3.4v1H138v-2.1h9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.9 0-1.6.2-2.2.6a4 4 0 0 0-1.3 1.6c-.4.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.5 2.5.4.7.8 1.2 1.5 1.6.6.4 1.3.5 2.2.5.5 0 1 0 1.4-.2.5-.2.9-.4 1.2-.7.3-.3.6-.7.8-1.2l2.7.5a5 5 0 0 1-1.2 2.1c-.6.6-1.3 1-2.1 1.4-.8.3-1.8.5-2.9.5Zm12.7-.3-4.3-14.7h3l2.8 10.8h.2l2.9-10.8h3l2.8 10.7h.1l2.9-10.7h3L168 40h-2.9l-3-10.6h-.2L159 40h-2.9Zm32.4.3c-1.3 0-2.6-.3-3.6-1-1-.6-1.8-1.5-2.4-2.6a8.8 8.8 0 0 1-.8-4c0-1.5.3-2.9.8-4a6.4 6.4 0 0 1 6-3.6c1.4 0 2.6.3 3.7 1 1 .6 1.8 1.5 2.3 2.6.6 1.1.9 2.5.9 4s-.3 2.9-.9 4a6.4 6.4 0 0 1-6 3.6Zm0-2.4c1 0 1.7-.2 2.3-.7.6-.5 1-1.1 1.3-2 .3-.7.4-1.6.4-2.5 0-1-.1-1.8-.4-2.6-.3-.8-.7-1.4-1.3-1.9-.6-.5-1.4-.7-2.3-.7-.9 0-1.6.2-2.2.7-.6.5-1 1.1-1.3 2-.3.7-.4 1.6-.4 2.5 0 1 .1 1.8.4 2.6.3.8.7 1.4 1.3 1.9.6.5 1.3.7 2.2.7Zm13-6.6V40h-2.9V25.3h2.8v2.4h.1c.4-.8.9-1.4 1.6-2a5 5 0 0 1 2.8-.6c1 0 1.9.2 2.6.6.8.4 1.4 1 1.8 1.9.4.8.6 1.8.6 3V40H208v-9c0-1-.3-2-.8-2.5a3 3 0 0 0-2.3-1c-.7 0-1.3.2-1.8.5s-.9.7-1.2 1.3c-.3.5-.4 1.2-.4 2Z" />
|
||||||
|
<path fill="#3BDB78"
|
||||||
|
d="M27 52a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1V53a1 1 0 0 0-1-1H27Zm4 4h2a1 1 0 0 1 1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-3h-7a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V57a1 1 0 0 1 1-1Zm12 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm12.8 0h2.4a1 1 0 0 1 .8.5l5 7.8 5-7.8a1 1 0 0 1 .8-.5h2.4a1 1 0 0 1 .8 1.5l-6.8 10.8a1 1 0 0 0-.2.6V75a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6.1c0-.2 0-.4-.2-.6L55 57.5a1 1 0 0 1 .8-1.5ZM83 56h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H82v12h15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H83a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1V61a1 1 0 0 1 1-1h3v-3a1 1 0 0 1 1-1Zm24 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V59.6l9.2 15.9c.2.3.5.5.8.5h5a1 1 0 0 0 1-1V57a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v15.4l-9.2-15.9a1 1 0 0 0-.8-.5h-5Zm29 0a1 1 0 0 0-1 1v3h12v-3a1 1 0 0 0-1-1h-10Zm11 4v12h3a1 1 0 0 0 1-1V61a1 1 0 0 0-1-1h-3Zm0 12h-12v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3Zm-12 0V60h-3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3Zm21-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h4v15a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V60h4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-18Zm27 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V63.4l5.5 12a1 1 0 0 0 1 .6h3a1 1 0 0 0 1-.6l5.5-12V75a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V57a1 1 0 0 0-1-1h-3.4a1 1 0 0 0-.9.6L194 71.2l-6.7-14.6a1 1 0 0 0-1-.6H183Zm32 0a1 1 0 0 0-1 1v3h15a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14Zm-1 4h-3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14a1 1 0 0 1-1-1v-3h7a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-7v-4Zm-38 12a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
8
web/src/assets/review-badge/short-white.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="48" width="128" viewBox="0 0 256 96">
|
||||||
|
<rect width="255" height="95" x=".5" y=".5" fill="#fff" stroke="#ECF0EE" rx="7.5" />
|
||||||
|
<path fill="#28AE5B" d="m56.5 16 4.1 8.3 9.2 1.4-6.6 6.5 1.5 9.1-8.2-4.3-8.2 4.3 1.5-9.1-6.6-6.5 9.2-1.4 4.1-8.3Z" />
|
||||||
|
<path fill="#3F3F3F"
|
||||||
|
d="M82.7 40V20.4h7c1.5 0 2.7.2 3.7.7 1 .6 1.8 1.3 2.3 2.2.5 1 .8 2 .8 3.2 0 1.2-.3 2.3-.8 3.2-.5.9-1.3 1.6-2.3 2.1-1 .5-2.2.8-3.8.8h-5.3V30h5c1 0 1.8-.1 2.4-.4.6-.3 1-.7 1.4-1.2a4 4 0 0 0 .4-1.9 4 4 0 0 0-.5-2c-.2-.5-.7-.9-1.3-1.2-.6-.2-1.4-.4-2.4-.4h-3.7V40h-3Zm9.7-8.9 4.8 8.9h-3.4l-4.7-8.9h3.3Zm13.6 9.2c-1.5 0-2.7-.3-3.8-1-1-.6-1.8-1.4-2.4-2.6a9 9 0 0 1-.8-4 9 9 0 0 1 .8-4c.6-1.1 1.4-2 2.4-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.3 1 .5 2 .5 3.4v1h-11.7v-2.1h8.9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.8 0-1.5.2-2.1.6a4 4 0 0 0-1.4 1.6c-.3.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.6 2.5.3.7.8 1.2 1.4 1.6.7.4 1.4.5 2.2.5.6 0 1 0 1.5-.2l1.2-.7c.3-.3.5-.7.7-1.2l2.7.5a5 5 0 0 1-1.1 2.1c-.6.6-1.3 1-2.1 1.4-.9.3-1.8.5-3 .5Zm21.7-15L122.3 40h-3l-5.4-14.7h3l3.8 11.3h.2l3.7-11.3h3Zm2.7 14.7V25.3h2.8V40h-2.8Zm1.4-17c-.5 0-1-.2-1.3-.5-.3-.3-.5-.7-.5-1.2s.2-.9.5-1.2c.4-.4.8-.5 1.3-.5s1 .1 1.3.5c.3.3.5.7.5 1.2s-.2.9-.5 1.2c-.4.3-.8.5-1.3.5Zm11.6 17.3c-1.4 0-2.7-.3-3.7-1-1-.6-1.9-1.4-2.4-2.6a9 9 0 0 1-.9-4 9 9 0 0 1 .9-4c.5-1.1 1.3-2 2.3-2.7a7.2 7.2 0 0 1 6-.6 5.9 5.9 0 0 1 3.6 3.7c.4 1 .6 2 .6 3.4v1H138v-2.1h9c0-.8-.2-1.5-.5-2a3.5 3.5 0 0 0-3.2-2c-.9 0-1.6.2-2.2.6a4 4 0 0 0-1.3 1.6c-.4.6-.5 1.3-.5 2v1.7c0 1 .2 1.8.5 2.5.4.7.8 1.2 1.5 1.6.6.4 1.3.5 2.2.5.5 0 1 0 1.4-.2.5-.2.9-.4 1.2-.7.3-.3.6-.7.8-1.2l2.7.5a5 5 0 0 1-1.2 2.1c-.6.6-1.3 1-2.1 1.4-.8.3-1.8.5-2.9.5Zm12.7-.3-4.3-14.7h3l2.8 10.8h.2l2.9-10.8h3l2.8 10.7h.1l2.9-10.7h3L168 40h-2.9l-3-10.6h-.2L159 40h-2.9Zm32.4.3c-1.3 0-2.6-.3-3.6-1-1-.6-1.8-1.5-2.4-2.6a8.8 8.8 0 0 1-.8-4c0-1.5.3-2.9.8-4a6.4 6.4 0 0 1 6-3.6c1.4 0 2.6.3 3.7 1 1 .6 1.8 1.5 2.3 2.6.6 1.1.9 2.5.9 4s-.3 2.9-.9 4a6.4 6.4 0 0 1-6 3.6Zm0-2.4c1 0 1.7-.2 2.3-.7.6-.5 1-1.1 1.3-2 .3-.7.4-1.6.4-2.5 0-1-.1-1.8-.4-2.6-.3-.8-.7-1.4-1.3-1.9-.6-.5-1.4-.7-2.3-.7-.9 0-1.6.2-2.2.7-.6.5-1 1.1-1.3 2-.3.7-.4 1.6-.4 2.5 0 1 .1 1.8.4 2.6.3.8.7 1.4 1.3 1.9.6.5 1.3.7 2.2.7Zm13-6.6V40h-2.9V25.3h2.8v2.4h.1c.4-.8.9-1.4 1.6-2a5 5 0 0 1 2.8-.6c1 0 1.9.2 2.6.6.8.4 1.4 1 1.8 1.9.4.8.6 1.8.6 3V40H208v-9c0-1-.3-2-.8-2.5a3 3 0 0 0-2.3-1c-.7 0-1.3.2-1.8.5s-.9.7-1.2 1.3c-.3.5-.4 1.2-.4 2Z" />
|
||||||
|
<path fill="#28AE5B"
|
||||||
|
d="M27 52a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1V53a1 1 0 0 0-1-1H27Zm4 4h2a1 1 0 0 1 1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-3h-7a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V57a1 1 0 0 1 1-1Zm12 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm12.8 0h2.4a1 1 0 0 1 .8.5l5 7.8 5-7.8a1 1 0 0 1 .8-.5h2.4a1 1 0 0 1 .8 1.5l-6.8 10.8a1 1 0 0 0-.2.6V75a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6.1c0-.2 0-.4-.2-.6L55 57.5a1 1 0 0 1 .8-1.5ZM83 56h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H82v12h15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H83a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1V61a1 1 0 0 1 1-1h3v-3a1 1 0 0 1 1-1Zm24 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V59.6l9.2 15.9c.2.3.5.5.8.5h5a1 1 0 0 0 1-1V57a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v15.4l-9.2-15.9a1 1 0 0 0-.8-.5h-5Zm29 0a1 1 0 0 0-1 1v3h12v-3a1 1 0 0 0-1-1h-10Zm11 4v12h3a1 1 0 0 0 1-1V61a1 1 0 0 0-1-1h-3Zm0 12h-12v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3Zm-12 0V60h-3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3Zm21-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h4v15a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V60h4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-18Zm27 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V63.4l5.5 12a1 1 0 0 0 1 .6h3a1 1 0 0 0 1-.6l5.5-12V75a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V57a1 1 0 0 0-1-1h-3.4a1 1 0 0 0-.9.6L194 71.2l-6.7-14.6a1 1 0 0 0-1-.6H183Zm32 0a1 1 0 0 0-1 1v3h15a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14Zm-1 4h-3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14a1 1 0 0 1-1-1v-3h7a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-7v-4Zm-38 12a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -6,8 +6,9 @@ import { pwaAssetsHead } from 'virtual:pwa-assets/head'
|
|||||||
import { pwaInfo } from 'virtual:pwa-info'
|
import { pwaInfo } from 'virtual:pwa-info'
|
||||||
|
|
||||||
import { isNotArray } from '../lib/arrays'
|
import { isNotArray } from '../lib/arrays'
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
|
|
||||||
|
import DevToolsMessageScript from './DevToolsMessageScript.astro'
|
||||||
import DynamicFavicon from './DynamicFavicon.astro'
|
import DynamicFavicon from './DynamicFavicon.astro'
|
||||||
import HtmxScript from './HtmxScript.astro'
|
import HtmxScript from './HtmxScript.astro'
|
||||||
import NotificationEventsScript from './NotificationEventsScript.astro'
|
import NotificationEventsScript from './NotificationEventsScript.astro'
|
||||||
@@ -106,8 +107,8 @@ const ogImageUrl = makeOgImageUrl(ogImage, Astro.url)
|
|||||||
|
|
||||||
<DynamicFavicon />
|
<DynamicFavicon />
|
||||||
|
|
||||||
<!-- Components -->
|
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
|
|
||||||
<LoadingIndicator color="green" />
|
<LoadingIndicator color="green" />
|
||||||
<TailwindJsPluggin />
|
<TailwindJsPluggin />
|
||||||
{htmx && <HtmxScript />}
|
{htmx && <HtmxScript />}
|
||||||
@@ -147,3 +148,5 @@ const ogImageUrl = makeOgImageUrl(ogImage, Astro.url)
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<DevToolsMessageScript />
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const ActualTag = disabled && Tag === 'a' ? 'span' : Tag
|
|||||||
class={base({ class: cn({ 'opacity-20 hover:opacity-50': disabled }, className) })}
|
class={base({ class: cn({ 'opacity-20 hover:opacity-50': disabled }, className) })}
|
||||||
role={role ?? (Tag === 'button' || Tag === 'label' || (disabled && Tag === 'a') ? undefined : 'button')}
|
role={role ?? (Tag === 'button' || Tag === 'label' || (disabled && Tag === 'a') ? undefined : 'button')}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
aria-label={label}
|
||||||
{...dataAstroReload && { 'data-astro-reload': dataAstroReload }}
|
{...dataAstroReload && { 'data-astro-reload': dataAstroReload }}
|
||||||
{...htmlProps}
|
{...htmlProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '../lib/commentsWithReplies'
|
} from '../lib/commentsWithReplies'
|
||||||
import { computeKarmaUnlocks } from '../lib/karmaUnlocks'
|
import { computeKarmaUnlocks } from '../lib/karmaUnlocks'
|
||||||
import { formatDateShort } from '../lib/timeAgo'
|
import { formatDateShort } from '../lib/timeAgo'
|
||||||
|
import { urlDomain } from '../lib/urls'
|
||||||
|
|
||||||
import BadgeSmall from './BadgeSmall.astro'
|
import BadgeSmall from './BadgeSmall.astro'
|
||||||
import CommentModeration from './CommentModeration.astro'
|
import CommentModeration from './CommentModeration.astro'
|
||||||
@@ -150,13 +151,13 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
checked={comment.suspicious}
|
checked={comment.suspicious}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="comment-header scrollbar-w-none -mt-10 flex items-center gap-2 overflow-auto pt-10 text-sm">
|
<div class="comment-header flex items-center gap-2 text-sm">
|
||||||
<label for={`collapse-${comment.id.toString()}`} class="cursor-pointer text-zinc-500 hover:text-zinc-300">
|
<label for={`collapse-${comment.id.toString()}`} class="cursor-pointer text-zinc-500 hover:text-zinc-300">
|
||||||
<span class="collapse-symbol text-xs"></span>
|
<span class="collapse-symbol text-xs"></span>
|
||||||
<span class="sr-only">Toggle comment visibility</span>
|
<span class="sr-only">Toggle comment visibility</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex min-w-16 items-center gap-1">
|
||||||
<UserBadge
|
<UserBadge
|
||||||
user={comment.author}
|
user={comment.author}
|
||||||
size="md"
|
size="md"
|
||||||
@@ -170,7 +171,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
comment.author.admin || comment.author.moderator
|
comment.author.admin || comment.author.moderator
|
||||||
? `KYCnot.me ${comment.author.admin ? 'Admin' : 'Moderator'}${comment.author.verifiedLink ? '. ' : ''}`
|
? `KYCnot.me ${comment.author.admin ? 'Admin' : 'Moderator'}${comment.author.verifiedLink ? '. ' : ''}`
|
||||||
: ''
|
: ''
|
||||||
}${comment.author.verifiedLink ? `Related to ${comment.author.verifiedLink}` : ''}`}
|
}${comment.author.verifiedLink ? `Related to ${urlDomain(comment.author.verifiedLink)}` : ''}`}
|
||||||
>
|
>
|
||||||
<Icon name="ri:verified-badge-fill" class="size-4 text-cyan-300" />
|
<Icon name="ri:verified-badge-fill" class="size-4 text-cyan-300" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -179,7 +180,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* User badges - more compact but still with text */}
|
{/* User badges - more compact but still with text */}
|
||||||
<div class="flex flex-wrap items-center gap-1">
|
<div class="flex w-min grow flex-wrap items-center gap-1">
|
||||||
{
|
{
|
||||||
comment.author.admin && (
|
comment.author.admin && (
|
||||||
<BadgeSmall icon="ri:shield-star-fill" color="green" text="Admin" variant="faded" inlineIcon />
|
<BadgeSmall icon="ri:shield-star-fill" color="green" text="Admin" variant="faded" inlineIcon />
|
||||||
|
|||||||
@@ -71,12 +71,13 @@ const [dbComments, pendingCommentsCount, activeRatingComment] = await Astro.loca
|
|||||||
'Failed to fetch comments',
|
'Failed to fetch comments',
|
||||||
async () =>
|
async () =>
|
||||||
await prisma.comment.findMany(
|
await prisma.comment.findMany(
|
||||||
makeCommentsNestedQuery({
|
await makeCommentsNestedQuery({
|
||||||
depth: MAX_COMMENT_DEPTH,
|
depth: MAX_COMMENT_DEPTH,
|
||||||
user,
|
user,
|
||||||
showPending: params.showPending,
|
showPending: params.showPending,
|
||||||
serviceId: service.id,
|
serviceId: service.id,
|
||||||
sort: params.sort,
|
sort: params.sort,
|
||||||
|
highlightedCommentId: params.comment,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[],
|
[],
|
||||||
|
|||||||
51
web/src/components/DevToolsMessageScript.astro
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { SOURCE_CODE_URL } from 'astro:env/client'
|
||||||
|
|
||||||
|
const logoStyle = `
|
||||||
|
padding: 0 119.5px;
|
||||||
|
display: block;
|
||||||
|
line-height: 64px;
|
||||||
|
background-size: auto 64px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50% 0;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 240 64'%3E%3Crect width='239' height='63' x='.5' y='.5' fill='%23101413' stroke='%23292B2A' rx='7.5'/%3E%3Cpath fill='%233BDB78' d='M19 18a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1H19Zm4 4h2a1 1 0 0 1 1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v3h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-3h-7a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V23a1 1 0 0 1 1-1Zm12 0h3a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1Zm12.8 0h2.4a1 1 0 0 1 .8.5l5 7.8 5-7.8a1 1 0 0 1 .8-.5h2.4a1 1 0 0 1 .8 1.5l-6.9 10.8a1 1 0 0 0-.1.6V41a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6.1c0-.2 0-.4-.2-.6L47 23.5a1 1 0 0 1 .8-1.5ZM75 22h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H74v12h15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H75a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1V27a1 1 0 0 1 1-1h3v-3a1 1 0 0 1 1-1Zm24 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V25.6l9.2 15.9c.2.3.5.5.8.5h5a1 1 0 0 0 1-1V23a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v15.4l-9.2-15.9a1 1 0 0 0-.8-.5h-5Zm29 0a1 1 0 0 0-1 1v3h12v-3a1 1 0 0 0-1-1h-10Zm11 4v12h3a1 1 0 0 0 1-1V27a1 1 0 0 0-1-1h-3Zm0 12h-12v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3Zm-12 0V26h-3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3Zm21-16a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h4v15a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V26h4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-18Zm27 0a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V29.4l5.5 12a1 1 0 0 0 1 .6h3a1 1 0 0 0 1-.6l5.5-12V41a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V23a1 1 0 0 0-1-1h-3.4a1 1 0 0 0-.9.6L186 37.2l-6.7-14.6a1 1 0 0 0-1-.6H175Zm32 0a1 1 0 0 0-1 1v3h15a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14Zm-1 4h-3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-14a1 1 0 0 1-1-1v-3h7a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-7v-4Zm-38 12a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-2Z'/%3E%3C/svg%3E");
|
||||||
|
`
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
console.log.bind(
|
||||||
|
console,
|
||||||
|
`\n%c \n%c\n 👋%c Hi there! %c\n\n‣ We included source maps, so you can easily inspect the code. 🕵🏻♂️\n‣ Everything works with JavaScript disabled.\n‣ Source code: ${SOURCE_CODE_URL}`,
|
||||||
|
logoStyle,
|
||||||
|
`
|
||||||
|
font-family: cursive;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
font-family: cursive;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#d97706 0%,
|
||||||
|
#f59e0b 20%,
|
||||||
|
#f97316 40%,
|
||||||
|
#ea580c 60%,
|
||||||
|
#f97316 80%,
|
||||||
|
#f59e0b 100%
|
||||||
|
) -100%/ 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
import { prisma } from '../lib/prisma'
|
import { prisma } from '../lib/prisma'
|
||||||
|
|
||||||
const user = Astro.locals.user
|
const user = Astro.locals.user
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
import { SOURCE_CODE_URL, I2P_ADDRESS, ONION_ADDRESS } from 'astro:env/server'
|
import { SOURCE_CODE_URL } from 'astro:env/client'
|
||||||
|
import { I2P_ADDRESS, ONION_ADDRESS } from 'astro:env/server'
|
||||||
|
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Icon } from 'astro-icon/components'
|
|||||||
import { sample } from 'lodash-es'
|
import { sample } from 'lodash-es'
|
||||||
|
|
||||||
import { splashTexts } from '../constants/splashTexts'
|
import { splashTexts } from '../constants/splashTexts'
|
||||||
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
|
||||||
import { makeLoginUrl, makeUnimpersonateUrl } from '../lib/redirectUrls'
|
import { makeLoginUrl, makeUnimpersonateUrl } from '../lib/redirectUrls'
|
||||||
|
|
||||||
import AdminOnly from './AdminOnly.astro'
|
import AdminOnly from './AdminOnly.astro'
|
||||||
@@ -123,6 +123,7 @@ const splashText = showSplashText ? sample(splashTexts) : null
|
|||||||
transition:name="header-admin-link"
|
transition:name="header-admin-link"
|
||||||
text="Admin Dashboard"
|
text="Admin Dashboard"
|
||||||
position="left"
|
position="left"
|
||||||
|
aria-label="Admin Dashboard"
|
||||||
>
|
>
|
||||||
<Icon name="ri:home-gear-line" class="size-10" />
|
<Icon name="ri:home-gear-line" class="size-10" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Props<Multiple extends boolean = false> = Omit<
|
|||||||
iconClass?: string
|
iconClass?: string
|
||||||
description?: MarkdownString
|
description?: MarkdownString
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
noTransitionPersist?: boolean
|
|
||||||
}[]
|
}[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
selectedValue?: Multiple extends true ? string[] : string
|
selectedValue?: Multiple extends true ? string[] : string
|
||||||
@@ -70,7 +69,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
transition:persist={option.noTransitionPersist || !multiple ? undefined : true}
|
transition:persist
|
||||||
type={multiple ? 'checkbox' : 'radio'}
|
type={multiple ? 'checkbox' : 'radio'}
|
||||||
name={wrapperProps.name}
|
name={wrapperProps.name}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
|
|||||||
@@ -4,14 +4,17 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { isBrowserNotificationsEnabled, showBrowserNotification } from '../lib/client/browserNotifications'
|
import { isBrowserNotificationsEnabled, showBrowserNotification } from '../lib/client/browserNotifications'
|
||||||
import { makeNotificationOptions } from '../lib/notificationOptions'
|
import {
|
||||||
|
makeBrowserNotificationOptions,
|
||||||
|
makeBrowserNotificationTitle,
|
||||||
|
} from '../lib/client/notificationOptions'
|
||||||
|
|
||||||
document.addEventListener('sse:new-notification', (event) => {
|
document.addEventListener('sse:new-notification', (event) => {
|
||||||
if (isBrowserNotificationsEnabled()) {
|
if (isBrowserNotificationsEnabled()) {
|
||||||
const payload = event.detail
|
const payload = event.detail
|
||||||
const notification = showBrowserNotification(
|
const notification = showBrowserNotification(
|
||||||
payload.title,
|
makeBrowserNotificationTitle(payload.title),
|
||||||
makeNotificationOptions(payload, { removeActions: true })
|
makeBrowserNotificationOptions(payload, { removeActions: true })
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle notification click
|
// Handle notification click
|
||||||
|
|||||||
151
web/src/components/PressAssets.astro
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
import favicon from '../../public/favicon.svg'
|
||||||
|
import logoMiniFull from '../assets/logo/logo-mini-full.svg'
|
||||||
|
import logoNormal from '../assets/logo/logo-normal.svg'
|
||||||
|
import logoSmall from '../assets/logo/logo-small.svg'
|
||||||
|
import reviewBadgeLongBlack from '../assets/review-badge/long-black.svg'
|
||||||
|
import reviewBadgeLongWhite from '../assets/review-badge/long-white.svg'
|
||||||
|
import reviewBadgeShortBlack from '../assets/review-badge/short-black.svg'
|
||||||
|
import reviewBadgeShortWhite from '../assets/review-badge/short-white.svg'
|
||||||
|
|
||||||
|
import Button from './Button.astro'
|
||||||
|
import MyPicture from './MyPicture.astro'
|
||||||
|
|
||||||
|
const categories: {
|
||||||
|
title: string
|
||||||
|
assets: {
|
||||||
|
name: string
|
||||||
|
path: typeof logoNormal
|
||||||
|
alt: string
|
||||||
|
}[]
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
title: 'Logos',
|
||||||
|
assets: [
|
||||||
|
{
|
||||||
|
name: 'Logo',
|
||||||
|
path: logoNormal,
|
||||||
|
alt: 'KYCnot.me logo normal version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logo small',
|
||||||
|
path: logoSmall,
|
||||||
|
alt: 'KYCnot.me logo small version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logo mini',
|
||||||
|
path: logoMiniFull,
|
||||||
|
alt: 'KYCnot.me logo mini version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logo icon',
|
||||||
|
path: favicon,
|
||||||
|
alt: 'KYCnot.me logo icon version',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Review badges',
|
||||||
|
assets: [
|
||||||
|
{
|
||||||
|
name: 'Review badge (long black)',
|
||||||
|
path: reviewBadgeLongBlack,
|
||||||
|
alt: 'KYCnot.me review badge long black version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Review badge (long white)',
|
||||||
|
path: reviewBadgeLongWhite,
|
||||||
|
alt: 'KYCnot.me review badge long white version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Review badge (short black)',
|
||||||
|
path: reviewBadgeShortBlack,
|
||||||
|
alt: 'KYCnot.me review badge short black version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Review badge (short white)',
|
||||||
|
path: reviewBadgeShortWhite,
|
||||||
|
alt: 'KYCnot.me review badge short white version',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="not-prose mb-16 space-y-8">
|
||||||
|
{
|
||||||
|
categories.map((category) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 class="font-title mb-2 text-center text-xl font-semibold text-white">{category.title}</h3>
|
||||||
|
<ul class="xs:grid-cols-2 grid grid-cols-1 gap-6">
|
||||||
|
{category.assets.map((asset) => (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="bg-transparency-grid mx-auto flex aspect-[3/1] max-w-sm items-center justify-center rounded-lg p-4"
|
||||||
|
style={{
|
||||||
|
'--transparency-grid-color-1': 'var(--color-night-600)',
|
||||||
|
'--transparency-grid-color-2': 'var(--color-night-500)',
|
||||||
|
'--transparency-grid-size': 'calc(var(--spacing) * 4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MyPicture
|
||||||
|
src={asset.path}
|
||||||
|
alt={asset.alt}
|
||||||
|
pictureAttributes={{
|
||||||
|
class: 'contents',
|
||||||
|
}}
|
||||||
|
class="max-h-full min-h-8 max-w-full min-w-8 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-center">
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
href={asset.path.src}
|
||||||
|
download={asset.name}
|
||||||
|
label={asset.name}
|
||||||
|
size="sm"
|
||||||
|
color="white"
|
||||||
|
variant="faded"
|
||||||
|
icon="ri:download-line"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-transparency-grid {
|
||||||
|
--transparency-grid-color-1: #fff;
|
||||||
|
--transparency-grid-color-2: #ccc;
|
||||||
|
--transparency-grid-size: calc(var(--spacing) * 8);
|
||||||
|
|
||||||
|
background-color: var(--transparency-grid-color-1);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--transparency-grid-color-2) 25%,
|
||||||
|
transparent 25%,
|
||||||
|
transparent 75%,
|
||||||
|
var(--transparency-grid-color-2) 75%,
|
||||||
|
var(--transparency-grid-color-2)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--transparency-grid-color-2) 25%,
|
||||||
|
transparent 25%,
|
||||||
|
transparent 75%,
|
||||||
|
var(--transparency-grid-color-2) 75%,
|
||||||
|
var(--transparency-grid-color-2)
|
||||||
|
);
|
||||||
|
background-size: var(--transparency-grid-size) var(--transparency-grid-size);
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
calc(var(--transparency-grid-size) / 2) calc(var(--transparency-grid-size) / 2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,6 @@ type Props = HTMLAttributes<'div'> & {
|
|||||||
pushSubscriptions: Prisma.PushSubscriptionGetPayload<{
|
pushSubscriptions: Prisma.PushSubscriptionGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
endpoint: true
|
endpoint: true
|
||||||
userAgent: true
|
|
||||||
}
|
}
|
||||||
}>[]
|
}>[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ if (!Astro.locals.user) return
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
|
// NOTE: Disable sse: events when user is not logged in
|
||||||
|
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||||
|
stopServerEventsListener()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data as string)
|
const data = JSON.parse(event.data as string)
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import { unsubscribeFromPushNotifications } from '../lib/client/clientPushNotifications'
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
interface Window {
|
|
||||||
__SW_REGISTRATION__?: ServiceWorkerRegistration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NO_AUTO_RELOAD_ROUTES = ['/account/welcome', '/500', '/404'] as const satisfies `/${string}`[]
|
const NO_AUTO_RELOAD_ROUTES = ['/account/welcome', '/500', '/404'] as const satisfies `/${string}`[]
|
||||||
|
|
||||||
@@ -40,7 +34,7 @@
|
|||||||
|
|
||||||
function shouldSkipAutoReload() {
|
function shouldSkipAutoReload() {
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
const isErrorPage = document.querySelector('[data-is-error-page]') !== null
|
const isErrorPage = document.body.hasAttribute('data-is-error-page')
|
||||||
|
|
||||||
return isErrorPage || NO_AUTO_RELOAD_ROUTES.some((route) => currentPath === route)
|
return isErrorPage || NO_AUTO_RELOAD_ROUTES.some((route) => currentPath === route)
|
||||||
}
|
}
|
||||||
@@ -51,4 +45,15 @@
|
|||||||
void updateSW(true)
|
void updateSW(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeinstallprompt', (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('astro:page-load', async () => {
|
||||||
|
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||||
|
await unsubscribeFromPushNotifications()
|
||||||
|
window.__SW_REGISTRATION__?.unregister()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
import { differenceInDays, isPast } from 'date-fns'
|
|
||||||
|
|
||||||
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
|
import { verificationStepStatusesByValue } from '../constants/verificationStepStatus'
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
|
import { formatDaysAgo } from '../lib/timeAgo'
|
||||||
import TimeFormatted from './TimeFormatted.astro'
|
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
const RECENTLY_ADDED_DAYS = 7
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
service: Prisma.ServiceGetPayload<{
|
service: Prisma.ServiceGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
verificationStatus: true
|
verificationStatus: true
|
||||||
verificationProofMd: true
|
verificationProofMd: true
|
||||||
verificationSummary: true
|
verificationSummary: true
|
||||||
listedAt: true
|
approvedAt: true
|
||||||
|
isRecentlyApproved: true
|
||||||
createdAt: true
|
createdAt: true
|
||||||
verificationSteps: {
|
verificationSteps: {
|
||||||
select: {
|
select: {
|
||||||
@@ -29,9 +27,6 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { service } = Astro.props
|
const { service } = Astro.props
|
||||||
|
|
||||||
const listedDate = service.listedAt ?? service.createdAt
|
|
||||||
const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), listedDate) < RECENTLY_ADDED_DAYS
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -66,10 +61,10 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : wasRecentlyAdded ? (
|
) : service.isRecentlyApproved ? (
|
||||||
<div class="mb-3 rounded-md bg-red-900/50 p-2 text-sm text-red-400">
|
<div class="mb-3 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400">
|
||||||
This service was {service.listedAt === null ? 'added ' : 'listed '}{' '}
|
This service was approved
|
||||||
<TimeFormatted date={listedDate} daysUntilDate={RECENTLY_ADDED_DAYS} />
|
{service.approvedAt ? formatDaysAgo(service.approvedAt) : 'less than 15 days ago'}
|
||||||
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
||||||
caution.
|
caution.
|
||||||
<a
|
<a
|
||||||
@@ -86,7 +81,7 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
|
|||||||
Basic checks passed, but not fully verified.
|
Basic checks passed, but not fully verified.
|
||||||
<a
|
<a
|
||||||
href="/about#suggestion-review-process"
|
href="/about#suggestion-review-process"
|
||||||
class="text-yellow-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
class="text-blue-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
@@ -98,14 +93,29 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
|
|||||||
{
|
{
|
||||||
service.verificationStatus !== 'VERIFICATION_FAILED' &&
|
service.verificationStatus !== 'VERIFICATION_FAILED' &&
|
||||||
service.verificationSteps.some((step) => step.status === 'FAILED') && (
|
service.verificationSteps.some((step) => step.status === 'FAILED') && (
|
||||||
<div class="mb-3 flex items-center gap-2 rounded-md bg-red-900/50 p-2 text-sm text-red-400">
|
<a
|
||||||
|
href="#verification"
|
||||||
|
class="mb-3 flex items-center gap-2 rounded-md bg-red-900/50 p-2 text-sm text-red-400 transition-colors hover:bg-red-900/60"
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={verificationStatusesByValue.VERIFICATION_FAILED.icon}
|
name={verificationStatusesByValue.VERIFICATION_FAILED.icon}
|
||||||
class={cn('size-5', verificationStatusesByValue.VERIFICATION_FAILED.classNames.icon)}
|
class={cn('size-5', verificationStatusesByValue.VERIFICATION_FAILED.classNames.icon)}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>Some verification steps failed. Please review the details below.</span>
|
||||||
This service has failed one or more verification steps. Review the verification details carefully.
|
</a>
|
||||||
</span>
|
)
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
service.verificationStatus !== 'VERIFICATION_FAILED' &&
|
||||||
|
!service.verificationSteps.some((step) => step.status === 'FAILED') &&
|
||||||
|
service.verificationSteps.some((step) => step.status === 'WARNING') && (
|
||||||
|
<a
|
||||||
|
href="#verification"
|
||||||
|
class="mb-3 flex items-center gap-2 rounded-md bg-yellow-600/30 p-2 text-sm text-yellow-200 transition-colors hover:bg-yellow-600/40"
|
||||||
|
>
|
||||||
|
<Icon name={verificationStepStatusesByValue.WARNING.icon} class={cn('size-5 text-yellow-400')} />
|
||||||
|
<span>Some verification steps are marked as warnings.</span>
|
||||||
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type EventTypeInfo<T extends string | null | undefined = string> = {
|
|||||||
description: string
|
description: string
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: string
|
dot: string
|
||||||
|
banner?: string
|
||||||
}
|
}
|
||||||
icon: string
|
icon: string
|
||||||
color: TailwindColor
|
color: TailwindColor
|
||||||
@@ -34,6 +35,7 @@ export const {
|
|||||||
description: '',
|
description: '',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
||||||
|
banner: 'bg-zinc-900/50 text-zinc-300 hover:bg-zinc-800/60 focus-visible:bg-zinc-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:question-fill',
|
icon: 'ri:question-fill',
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
@@ -48,6 +50,7 @@ export const {
|
|||||||
description: 'Potential issues that users should be aware of',
|
description: 'Potential issues that users should be aware of',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
||||||
|
banner: 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60 focus-visible:bg-yellow-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:alert-fill',
|
icon: 'ri:alert-fill',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
@@ -61,6 +64,7 @@ export const {
|
|||||||
description: 'A previously reported warning has been solved',
|
description: 'A previously reported warning has been solved',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
||||||
|
banner: 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60 focus-visible:bg-yellow-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:alert-fill',
|
icon: 'ri:alert-fill',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
@@ -74,6 +78,7 @@ export const {
|
|||||||
description: 'Critical issues affecting service functionality',
|
description: 'Critical issues affecting service functionality',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
||||||
|
banner: 'bg-red-900/50 text-red-300 hover:bg-red-800/60 focus-visible:bg-red-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:spam-fill',
|
icon: 'ri:spam-fill',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
@@ -87,6 +92,7 @@ export const {
|
|||||||
description: 'A previously reported alert has been solved',
|
description: 'A previously reported alert has been solved',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
||||||
|
banner: 'bg-red-900/50 text-red-300 hover:bg-red-800/60 focus-visible:bg-red-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:spam-fill',
|
icon: 'ri:spam-fill',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
@@ -100,6 +106,7 @@ export const {
|
|||||||
description: 'General information about the service',
|
description: 'General information about the service',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-blue-900 text-blue-300 ring-blue-900/50',
|
dot: 'bg-blue-900 text-blue-300 ring-blue-900/50',
|
||||||
|
banner: 'bg-blue-900/50 text-blue-300 hover:bg-blue-800/60 focus-visible:bg-blue-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:information-fill',
|
icon: 'ri:information-fill',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
@@ -113,6 +120,7 @@ export const {
|
|||||||
description: 'Regular service update or announcement',
|
description: 'Regular service update or announcement',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
||||||
|
banner: 'bg-zinc-900/50 text-zinc-300 hover:bg-zinc-800/60 focus-visible:bg-zinc-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:notification-fill',
|
icon: 'ri:notification-fill',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
@@ -126,6 +134,7 @@ export const {
|
|||||||
description: 'Service details were updated on kycnot.me',
|
description: 'Service details were updated on kycnot.me',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-sky-900 text-sky-300 ring-sky-900/50',
|
dot: 'bg-sky-900 text-sky-300 ring-sky-900/50',
|
||||||
|
banner: 'bg-sky-900/50 text-sky-300 hover:bg-sky-800/60 focus-visible:bg-sky-800/60',
|
||||||
},
|
},
|
||||||
icon: 'ri:pencil-fill',
|
icon: 'ri:pencil-fill',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
||||||
import { transformCase } from '../lib/strings'
|
import { transformCase } from '../lib/strings'
|
||||||
|
|
||||||
import type { KycLevelClarification } from '@prisma/client'
|
import type { AttributeType, KycLevelClarification } from '@prisma/client'
|
||||||
|
|
||||||
type KycLevelClarificationInfo<T extends string | null | undefined = string> = {
|
type KycLevelClarificationInfo<T extends string | null | undefined = string> = {
|
||||||
value: T
|
value: T
|
||||||
|
slug: string
|
||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
icon: string
|
icon: string
|
||||||
|
privacyPoints: number
|
||||||
|
attributeType: AttributeType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
@@ -18,22 +21,31 @@ export const {
|
|||||||
'value',
|
'value',
|
||||||
(value): KycLevelClarificationInfo<typeof value> => ({
|
(value): KycLevelClarificationInfo<typeof value> => ({
|
||||||
value,
|
value,
|
||||||
|
slug: value ? value.toLowerCase().replace('_', '-') : '',
|
||||||
label: value ? transformCase(value.replace('_', ' '), 'title') : String(value),
|
label: value ? transformCase(value.replace('_', ' '), 'title') : String(value),
|
||||||
description: '',
|
description: '',
|
||||||
icon: 'ri:question-line',
|
icon: 'ri:question-line',
|
||||||
|
privacyPoints: 0,
|
||||||
|
attributeType: 'INFO',
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
value: 'NONE',
|
value: 'NONE',
|
||||||
|
slug: 'none',
|
||||||
label: 'None',
|
label: 'None',
|
||||||
description: 'No clarification needed.',
|
description: 'No clarification needed.',
|
||||||
icon: 'ri:file-copy-line',
|
icon: 'ri:file-copy-line',
|
||||||
|
privacyPoints: 0,
|
||||||
|
attributeType: 'INFO',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'DEPENDS_ON_PARTNERS',
|
value: 'DEPENDS_ON_PARTNERS',
|
||||||
|
slug: 'depends-on-partners',
|
||||||
label: 'Depends on partners',
|
label: 'Depends on partners',
|
||||||
description: 'May vary across partners.',
|
description: 'May vary across partners.',
|
||||||
icon: 'ri:share-forward-line',
|
icon: 'ri:share-forward-line',
|
||||||
|
privacyPoints: -5,
|
||||||
|
attributeType: 'WARNING',
|
||||||
},
|
},
|
||||||
] as const satisfies KycLevelClarificationInfo<KycLevelClarification>[]
|
] as const satisfies KycLevelClarificationInfo<KycLevelClarification>[]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export const {
|
|||||||
icon: 'ri:alert-line',
|
icon: 'ri:alert-line',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'WARNING',
|
||||||
|
label: 'Warning',
|
||||||
|
icon: 'ri:alert-line',
|
||||||
|
color: 'yellow',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'PENDING',
|
value: 'PENDING',
|
||||||
label: 'Pending',
|
label: 'Pending',
|
||||||
|
|||||||
1
web/src/env.d.ts
vendored
@@ -17,6 +17,7 @@ declare global {
|
|||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
htmx?: typeof htmx
|
htmx?: typeof htmx
|
||||||
|
__SW_REGISTRATION__?: ServiceWorkerRegistration
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace PrismaJson {
|
namespace PrismaJson {
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ const announcement = await Astro.locals.banners.try(
|
|||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class={cn('bg-night-700 text-day-300 flex min-h-dvh flex-col *:shrink-0', classNames?.body)}
|
class={cn('bg-night-700 text-day-300 flex min-h-dvh flex-col *:shrink-0', classNames?.body)}
|
||||||
data-is-error-page={isErrorPage}
|
data-is-error-page={isErrorPage ? '' : undefined}
|
||||||
|
data-is-logged-in={Astro.locals.user !== null ? '' : undefined}
|
||||||
>
|
>
|
||||||
{announcement && <AnnouncementBanner announcement={announcement} transition:name="header-announcement" />}
|
{announcement && <AnnouncementBanner announcement={announcement} transition:name="header-announcement" />}
|
||||||
<Header
|
<Header
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { makeOgImageUrl, type OgImageAllTemplatesWithProps } from '../components/OgImage'
|
import { makeOgImageUrl, type OgImageAllTemplatesWithProps } from '../components/OgImage'
|
||||||
|
import TimeFormatted from '../components/TimeFormatted.astro'
|
||||||
import { KYCNOTME_SCHEMA_MINI } from '../lib/schema'
|
import { KYCNOTME_SCHEMA_MINI } from '../lib/schema'
|
||||||
|
|
||||||
import BaseLayout from './BaseLayout.astro'
|
import BaseLayout from './BaseLayout.astro'
|
||||||
@@ -13,21 +14,19 @@ type Props = ComponentProps<typeof BaseLayout> &
|
|||||||
MarkdownLayoutProps<{
|
MarkdownLayoutProps<{
|
||||||
children: AstroChildren
|
children: AstroChildren
|
||||||
title: string
|
title: string
|
||||||
author: string
|
updatedAt?: string
|
||||||
pubDate: string
|
|
||||||
description: string
|
description: string
|
||||||
icon?: string
|
icon?: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
const { frontmatter, schemas, ...baseLayoutProps } = Astro.props
|
const { frontmatter, schemas, ...baseLayoutProps } = Astro.props
|
||||||
const publishDate = frontmatter.pubDate ? new Date(frontmatter.pubDate) : null
|
const publishDate = frontmatter.updatedAt ? new Date(frontmatter.updatedAt) : null
|
||||||
const ogImageTemplateData = {
|
const ogImageTemplateData = {
|
||||||
template: 'generic',
|
template: 'generic',
|
||||||
title: frontmatter.title,
|
title: frontmatter.title,
|
||||||
description: frontmatter.description,
|
description: frontmatter.description,
|
||||||
icon: frontmatter.icon,
|
icon: frontmatter.icon,
|
||||||
} satisfies OgImageAllTemplatesWithProps
|
} satisfies OgImageAllTemplatesWithProps
|
||||||
const weAreAuthor = frontmatter.author.toLowerCase().trim() === 'kycnot.me'
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
@@ -44,14 +43,7 @@ const weAreAuthor = frontmatter.author.toLowerCase().trim() === 'kycnot.me'
|
|||||||
datePublished: publishDate?.toISOString(),
|
datePublished: publishDate?.toISOString(),
|
||||||
dateModified: publishDate?.toISOString(),
|
dateModified: publishDate?.toISOString(),
|
||||||
image: makeOgImageUrl(ogImageTemplateData, Astro.url),
|
image: makeOgImageUrl(ogImageTemplateData, Astro.url),
|
||||||
author: frontmatter.author
|
author: KYCNOTME_SCHEMA_MINI,
|
||||||
? weAreAuthor
|
|
||||||
? KYCNOTME_SCHEMA_MINI
|
|
||||||
: {
|
|
||||||
'@type': 'Person',
|
|
||||||
name: frontmatter.author,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
publisher: KYCNOTME_SCHEMA_MINI,
|
publisher: KYCNOTME_SCHEMA_MINI,
|
||||||
mainEntityOfPage: {
|
mainEntityOfPage: {
|
||||||
'@type': 'WebPage',
|
'@type': 'WebPage',
|
||||||
@@ -61,15 +53,26 @@ const weAreAuthor = frontmatter.author.toLowerCase().trim() === 'kycnot.me'
|
|||||||
...(schemas ?? []),
|
...(schemas ?? []),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
<div class="bg-dots-fade absolute inset-x-0 top-0 -z-1 h-128 opacity-15"></div>
|
||||||
<div
|
<div
|
||||||
class="prose prose-invert prose-headings:text-green-400 prose-h1:text-[2.5rem] prose-h1:font-bold prose-h1:my-8 prose-h1:drop-shadow-[0_0_10px_rgba(0,255,0,0.3)] prose-h2:text-green-500 prose-h2:text-[1.8rem] prose-h2:font-semibold prose-h2:my-6 prose-h2:border-b prose-h2:border-green-900 prose-h2:pb-1 prose-h3:text-green-600 prose-h3:text-[1.4rem] prose-h3:font-semibold prose-h3:my-4 prose-h4:text-green-700 prose-h4:text-[1.2rem] prose-h4:font-semibold prose-h4:my-3 prose-strong:font-semibold prose-strong:drop-shadow-[0_0_5px_rgba(0,255,0,0.2)] prose-p:text-gray-300 prose-p:my-4 prose-p:leading-relaxed prose-a:text-green-400 prose-a:no-underline prose-a:transition-all prose-a:border-b prose-a:border-green-900 prose-a:hover:text-green-400 prose-a:hover:drop-shadow-[0_0_8px_rgba(0,255,0,0.4)] prose-a:hover:border-green-400 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:my-2 prose-li:leading-relaxed mx-auto"
|
class="prose prose-invert prose-headings:text-green-400 prose-h1:text-[2.5rem] prose-h1:font-bold prose-h1:my-8 prose-h1:drop-shadow-[0_0_10px_rgba(0,255,0,0.3)] prose-h2:text-green-500 prose-h2:text-[1.8rem] prose-h2:font-semibold prose-h2:my-6 prose-h2:border-b prose-h2:border-green-900 prose-h2:pb-1 prose-h3:text-green-600 prose-h3:text-[1.4rem] prose-h3:font-semibold prose-h3:my-4 prose-h4:text-green-700 prose-h4:text-[1.2rem] prose-h4:font-semibold prose-h4:my-3 prose-strong:font-semibold prose-strong:drop-shadow-[0_0_5px_rgba(0,255,0,0.2)] prose-p:text-gray-300 prose-p:my-4 prose-p:leading-relaxed prose-a:text-green-400 prose-a:no-underline prose-a:transition-all prose-a:border-b prose-a:border-green-900 prose-a:hover:text-green-400 prose-a:hover:drop-shadow-[0_0_8px_rgba(0,255,0,0.4)] prose-a:hover:border-green-400 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:my-2 prose-li:leading-relaxed mx-auto"
|
||||||
>
|
>
|
||||||
<h1>{frontmatter.title}</h1>
|
<h1 class="mb-0!">{frontmatter.title}</h1>
|
||||||
<p class="text-gray-500">
|
<p class="mt-2! opacity-70">
|
||||||
{frontmatter.author && `by ${frontmatter.author}`}
|
Updated {frontmatter.updatedAt && <TimeFormatted date={new Date(frontmatter.updatedAt)} />}
|
||||||
{frontmatter.pubDate && ` | ${new Date(frontmatter.pubDate).toLocaleDateString()}`}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-dots-fade {
|
||||||
|
background:
|
||||||
|
radial-gradient(closest-side, #777, #fff) 0/ 1em 1em space,
|
||||||
|
linear-gradient(to bottom, #888, #fff);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
filter: contrast(21);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ 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 { getKycLevelClarificationInfo } 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'
|
||||||
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
|
|
||||||
import { formatDateShort } from './timeAgo'
|
import { formatDaysAgo } from './timeAgo'
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ type NonDbAttributeFull = NonDbAttribute & {
|
|||||||
select: {
|
select: {
|
||||||
verificationStatus: true
|
verificationStatus: true
|
||||||
serviceVisibility: true
|
serviceVisibility: true
|
||||||
isRecentlyListed: true
|
isRecentlyApproved: true
|
||||||
listedAt: true
|
approvedAt: true
|
||||||
createdAt: true
|
createdAt: true
|
||||||
tosReviewAt: true
|
tosReviewAt: true
|
||||||
tosReview: true
|
tosReview: true
|
||||||
@@ -156,16 +156,25 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
icon: 'ri:search-line',
|
icon: 'ri:search-line',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
customize: (service) => {
|
customize: (service) => ({
|
||||||
const clarification = getKycLevelClarificationInfo(service.kycLevelClarification)
|
show: service.kycLevel === kycLevel.value,
|
||||||
return {
|
}),
|
||||||
show: service.kycLevel === kycLevel.value,
|
|
||||||
title: kycLevel.name + (clarification.value !== 'NONE' ? ` (${clarification.label})` : ''),
|
|
||||||
description:
|
|
||||||
kycLevel.description + (clarification.value !== 'NONE' ? ` ${clarification.description}` : ''),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})),
|
})),
|
||||||
|
...kycLevelClarifications
|
||||||
|
.filter((clarification) => clarification.value !== 'NONE')
|
||||||
|
.map<NonDbAttributeFull>((clarification) => ({
|
||||||
|
slug: `kyc-clarification-${clarification.slug}`,
|
||||||
|
title: `KYC ${clarification.label}`,
|
||||||
|
type: clarification.attributeType,
|
||||||
|
category: 'PRIVACY',
|
||||||
|
description: clarification.description,
|
||||||
|
privacyPoints: clarification.privacyPoints,
|
||||||
|
trustPoints: 0,
|
||||||
|
links: [],
|
||||||
|
customize: (service) => ({
|
||||||
|
show: service.kycLevelClarification === clarification.value,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
slug: 'archived',
|
slug: 'archived',
|
||||||
title: serviceVisibilitiesById.ARCHIVED.label,
|
title: serviceVisibilitiesById.ARCHIVED.label,
|
||||||
@@ -180,17 +189,17 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'recently-listed',
|
slug: 'recently-approved',
|
||||||
title: 'Recently listed',
|
title: 'Recently approved',
|
||||||
type: 'WARNING',
|
type: 'WARNING',
|
||||||
category: 'TRUST',
|
category: 'TRUST',
|
||||||
description: 'Listed on KYCnot.me less than 15 days ago. Proceed with caution.',
|
description: 'Approved on KYCnot.me less than 15 days ago. Proceed with caution.',
|
||||||
privacyPoints: 0,
|
privacyPoints: 0,
|
||||||
trustPoints: -5,
|
trustPoints: -5,
|
||||||
links: [],
|
links: [],
|
||||||
customize: (service) => ({
|
customize: (service) => ({
|
||||||
show: service.isRecentlyListed,
|
show: service.isRecentlyApproved,
|
||||||
description: `Listed on KYCnot.me ${formatDateShort(service.listedAt ?? service.createdAt)}. Proceed with caution.`,
|
description: `Approved on KYCnot.me less than 15 days ago${service.approvedAt ? ` (${formatDaysAgo(service.approvedAt)})` : ''}. Proceed with caution.`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -208,41 +217,22 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'has-onion-urls',
|
slug: 'has-onion-or-i2p-urls',
|
||||||
title: 'Has Onion URLs',
|
title: 'Has Onion or I2P URLs',
|
||||||
type: 'GOOD',
|
type: 'GOOD',
|
||||||
category: 'PRIVACY',
|
category: 'PRIVACY',
|
||||||
description: 'Onion (Tor) URLs enhance privacy and anonymity.',
|
description: 'Onion (Tor) and I2P URLs enhance privacy and anonymity.',
|
||||||
privacyPoints: 5,
|
privacyPoints: 5,
|
||||||
trustPoints: 0,
|
trustPoints: 0,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
url: '/?onion=true',
|
url: '/?networks=onion&networks=i2p',
|
||||||
label: 'Search with this',
|
label: 'Search with this',
|
||||||
icon: 'ri:search-line',
|
icon: 'ri:search-line',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
customize: (service) => ({
|
customize: (service) => ({
|
||||||
show: service.onionUrls.length > 0,
|
show: service.onionUrls.length > 0 || service.i2pUrls.length > 0,
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'has-i2p-urls',
|
|
||||||
title: 'Has I2P URLs',
|
|
||||||
type: 'GOOD',
|
|
||||||
category: 'PRIVACY',
|
|
||||||
description: 'I2P URLs enhance privacy and anonymity.',
|
|
||||||
privacyPoints: 5,
|
|
||||||
trustPoints: 0,
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
url: '/?i2p=true',
|
|
||||||
label: 'Search with this',
|
|
||||||
icon: 'ri:search-line',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
customize: (service) => ({
|
|
||||||
show: service.i2pUrls.length > 0,
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ export function supportsBrowserNotifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isBrowserNotificationsEnabled() {
|
export function isBrowserNotificationsEnabled() {
|
||||||
return (
|
const browserNotificationsEnabled = typedLocalStorage.browserNotificationsEnabled.get()
|
||||||
supportsBrowserNotifications() &&
|
if (!browserNotificationsEnabled) return false
|
||||||
Notification.permission === 'granted' &&
|
|
||||||
typedLocalStorage.browserNotificationsEnabled.get()
|
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||||
)
|
typedLocalStorage.browserNotificationsEnabled.set(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportsBrowserNotifications() && Notification.permission === 'granted'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enableBrowserNotifications(): Promise<SafeResult> {
|
export async function enableBrowserNotifications(): Promise<SafeResult> {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { actions } from 'astro:actions'
|
|||||||
|
|
||||||
type ServerSubscription = {
|
type ServerSubscription = {
|
||||||
endpoint: string
|
endpoint: string
|
||||||
userAgent: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SafeResult =
|
export type SafeResult =
|
||||||
@@ -45,7 +44,6 @@ export async function subscribeToPushNotifications(vapidPublicKey: string): Prom
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
p256dhKey: p256dh ? btoa(String.fromCharCode(...new Uint8Array(p256dh))) : '',
|
p256dhKey: p256dh ? btoa(String.fromCharCode(...new Uint8Array(p256dh))) : '',
|
||||||
authKey: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
|
authKey: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
|
||||||
} satisfies ActionInput<typeof actions.notification.webPush.subscribe>),
|
} satisfies ActionInput<typeof actions.notification.webPush.subscribe>),
|
||||||
@@ -131,13 +129,7 @@ export async function isCurrentDeviceSubscribed(serverSubscriptions: ServerSubsc
|
|||||||
const currentSubscription = await getCurrentSubscription()
|
const currentSubscription = await getCurrentSubscription()
|
||||||
if (!currentSubscription || serverSubscriptions.length === 0) return false
|
if (!currentSubscription || serverSubscriptions.length === 0) return false
|
||||||
|
|
||||||
const currentEndpoint = currentSubscription.endpoint
|
return serverSubscriptions.some((sub) => sub.endpoint === currentSubscription.endpoint)
|
||||||
const currentUserAgent = navigator.userAgent
|
|
||||||
|
|
||||||
return serverSubscriptions.some(
|
|
||||||
(sub) =>
|
|
||||||
sub.endpoint === currentEndpoint && (sub.userAgent === currentUserAgent || sub.userAgent === null)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlB64ToUint8Array(base64String: string) {
|
function urlB64ToUint8Array(base64String: string) {
|
||||||
@@ -183,5 +175,5 @@ export function parsePushSubscriptions(subscriptionsAsString: string | undefined
|
|||||||
function isServerSubscription(subscription: unknown): subscription is ServerSubscription {
|
function isServerSubscription(subscription: unknown): subscription is ServerSubscription {
|
||||||
if (typeof subscription !== 'object' || subscription === null) return false
|
if (typeof subscription !== 'object' || subscription === null) return false
|
||||||
const s = subscription as Record<string, unknown>
|
const s = subscription as Record<string, unknown>
|
||||||
return typeof s.endpoint === 'string' && (typeof s.userAgent === 'string' || s.userAgent === null)
|
return typeof s.endpoint === 'string'
|
||||||
}
|
}
|
||||||
|
|||||||
7
web/src/lib/client/envVariables.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const DEPLOYMENT_MODE = import.meta.env.PROD
|
||||||
|
? import.meta.env.MODE === 'development' ||
|
||||||
|
import.meta.env.MODE === 'staging' ||
|
||||||
|
import.meta.env.MODE === 'production'
|
||||||
|
? import.meta.env.MODE
|
||||||
|
: 'development'
|
||||||
|
: 'development'
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { NotificationData, NotificationPayload } from './serverEventsTypes'
|
import { DEPLOYMENT_MODE } from './envVariables'
|
||||||
|
|
||||||
|
import type { NotificationData, NotificationPayload } from '../serverEventsTypes'
|
||||||
|
|
||||||
export type CustomNotificationOptions = NotificationOptions & {
|
export type CustomNotificationOptions = NotificationOptions & {
|
||||||
actions?: { action: string; title: string; icon?: string }[]
|
actions?: { action: string; title: string; icon?: string }[]
|
||||||
@@ -6,14 +8,24 @@ export type CustomNotificationOptions = NotificationOptions & {
|
|||||||
data: NotificationData
|
data: NotificationData
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeNotificationOptions(
|
export function makeBrowserNotificationTitle(title?: string | null) {
|
||||||
|
const prefix = DEPLOYMENT_MODE === 'development' ? '[DEV] ' : DEPLOYMENT_MODE === 'staging' ? '[PRE] ' : ''
|
||||||
|
return `${prefix}${title ?? 'New Notification'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeBrowserNotificationOptions(
|
||||||
payload: NotificationPayload | null,
|
payload: NotificationPayload | null,
|
||||||
options: { removeActions?: boolean } = {}
|
options: { removeActions?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
const defaultOptions: CustomNotificationOptions = {
|
const defaultOptions: CustomNotificationOptions = {
|
||||||
body: 'You have a new notification',
|
body: 'You have a new notification',
|
||||||
lang: 'en-US',
|
lang: 'en-US',
|
||||||
icon: '/favicon.svg',
|
icon:
|
||||||
|
DEPLOYMENT_MODE === 'development'
|
||||||
|
? '/favicon-dev.svg'
|
||||||
|
: DEPLOYMENT_MODE === 'staging'
|
||||||
|
? '/favicon-stage.svg'
|
||||||
|
: '/favicon.svg',
|
||||||
badge: '/notification-icon.svg',
|
badge: '/notification-icon.svg',
|
||||||
requireInteraction: false,
|
requireInteraction: false,
|
||||||
silent: false,
|
silent: false,
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { prisma } from './prisma'
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
export const MAX_COMMENT_DEPTH = 12
|
export const MAX_COMMENT_DEPTH = 12
|
||||||
@@ -75,12 +77,13 @@ export type CommentWithRepliesPopulated = CommentWithReplies<{
|
|||||||
export const commentSortSchema = z.enum(['newest', 'upvotes', 'status']).default('newest')
|
export const commentSortSchema = z.enum(['newest', 'upvotes', 'status']).default('newest')
|
||||||
export type CommentSortOption = z.infer<typeof commentSortSchema>
|
export type CommentSortOption = z.infer<typeof commentSortSchema>
|
||||||
|
|
||||||
export function makeCommentsNestedQuery({
|
export async function makeCommentsNestedQuery({
|
||||||
depth = 0,
|
depth = 0,
|
||||||
user,
|
user,
|
||||||
showPending,
|
showPending,
|
||||||
serviceId,
|
serviceId,
|
||||||
sort,
|
sort,
|
||||||
|
highlightedCommentId,
|
||||||
}: {
|
}: {
|
||||||
depth?: number
|
depth?: number
|
||||||
user: Prisma.UserGetPayload<{
|
user: Prisma.UserGetPayload<{
|
||||||
@@ -91,6 +94,7 @@ export function makeCommentsNestedQuery({
|
|||||||
showPending?: boolean
|
showPending?: boolean
|
||||||
serviceId: number
|
serviceId: number
|
||||||
sort: CommentSortOption
|
sort: CommentSortOption
|
||||||
|
highlightedCommentId?: number | null
|
||||||
}) {
|
}) {
|
||||||
const orderByClause: Prisma.CommentOrderByWithRelationInput[] = []
|
const orderByClause: Prisma.CommentOrderByWithRelationInput[] = []
|
||||||
|
|
||||||
@@ -108,6 +112,8 @@ export function makeCommentsNestedQuery({
|
|||||||
}
|
}
|
||||||
orderByClause.unshift({ suspicious: 'asc' }) // Always put suspicious comments last within a sort group
|
orderByClause.unshift({ suspicious: 'asc' }) // Always put suspicious comments last within a sort group
|
||||||
|
|
||||||
|
const highlightedBranchIds = highlightedCommentId ? await findAllParentIds(highlightedCommentId, depth) : []
|
||||||
|
|
||||||
const baseQuery = {
|
const baseQuery = {
|
||||||
...commentReplyQuery,
|
...commentReplyQuery,
|
||||||
orderBy: orderByClause,
|
orderBy: orderByClause,
|
||||||
@@ -121,6 +127,9 @@ export function makeCommentsNestedQuery({
|
|||||||
: ({
|
: ({
|
||||||
status: { in: ['APPROVED', 'VERIFIED'] },
|
status: { in: ['APPROVED', 'VERIFIED'] },
|
||||||
} as const satisfies Prisma.CommentWhereInput),
|
} as const satisfies Prisma.CommentWhereInput),
|
||||||
|
...(highlightedBranchIds.length > 0
|
||||||
|
? [{ id: { in: highlightedBranchIds } } as const satisfies Prisma.CommentWhereInput]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
parentId: null,
|
parentId: null,
|
||||||
serviceId,
|
serviceId,
|
||||||
@@ -161,6 +170,47 @@ export function makeRepliesQuery<T extends Prisma.CommentFindManyArgs>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAllParentIds(commentId: number, depth: number) {
|
||||||
|
const commentwithManyParents = await prisma.comment.findFirst({
|
||||||
|
where: { id: commentId },
|
||||||
|
select: makeParentQuerySelect(depth),
|
||||||
|
})
|
||||||
|
|
||||||
|
return extractParentIds(commentwithManyParents, [commentId])
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentQueryRecursive = {
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
parent: false | { select: ParentQueryRecursive }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeParentQuerySelect(depth: number): ParentQueryRecursive {
|
||||||
|
return {
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
parent: depth <= 0 ? false : { select: makeParentQuerySelect(depth - 1) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Prisma.CommentSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractParentIds(
|
||||||
|
comment: Prisma.CommentGetPayload<{ select: ParentQueryRecursive }> | null,
|
||||||
|
acc: number[] = []
|
||||||
|
) {
|
||||||
|
if (!comment?.parent?.id) return acc
|
||||||
|
|
||||||
|
return extractParentIds(comment.parent as Prisma.CommentGetPayload<{ select: ParentQueryRecursive }>, [
|
||||||
|
...acc,
|
||||||
|
comment.parent.id,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
export function makeCommentUrl({
|
export function makeCommentUrl({
|
||||||
serviceSlug,
|
serviceSlug,
|
||||||
commentId,
|
commentId,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { z } from 'astro/zod'
|
|
||||||
|
|
||||||
const schema = z.enum(['development', 'staging', 'production'])
|
|
||||||
|
|
||||||
export const DEPLOYMENT_MODE = schema.parse(import.meta.env.PROD ? import.meta.env.MODE : 'development')
|
|
||||||
@@ -39,7 +39,6 @@ export async function getService(slug: string | undefined): Promise<
|
|||||||
const service =
|
const service =
|
||||||
(await prisma.service.findFirst({
|
(await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
slug,
|
slug,
|
||||||
},
|
},
|
||||||
@@ -47,7 +46,6 @@ export async function getService(slug: string | undefined): Promise<
|
|||||||
})) ??
|
})) ??
|
||||||
(await prisma.service.findFirst({
|
(await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
previousSlugs: { has: slug },
|
previousSlugs: { has: slug },
|
||||||
},
|
},
|
||||||
@@ -175,7 +173,6 @@ export async function getEvents(): Promise<
|
|||||||
where: {
|
where: {
|
||||||
visible: true,
|
visible: true,
|
||||||
service: {
|
service: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function stopImpersonating(context: Pick<APIContext, 'cookies' | 'l
|
|||||||
const sessionId = context.cookies.get(IMPERSONATION_SESSION_COOKIE)?.value
|
const sessionId = context.cookies.get(IMPERSONATION_SESSION_COOKIE)?.value
|
||||||
await redisImpersonationSessions.delete(sessionId)
|
await redisImpersonationSessions.delete(sessionId)
|
||||||
context.cookies.delete(IMPERSONATION_SESSION_COOKIE)
|
context.cookies.delete(IMPERSONATION_SESSION_COOKIE)
|
||||||
context.locals.user = context.locals.actualUser
|
context.locals.user = context.locals.actualUser ?? context.locals.user
|
||||||
context.locals.actualUser = null
|
context.locals.actualUser = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDays, format, isBefore, isToday, isYesterday } from 'date-fns'
|
import { addDays, differenceInDays, format, isBefore, isToday, isYesterday } from 'date-fns'
|
||||||
import TimeAgo from 'javascript-time-ago'
|
import TimeAgo from 'javascript-time-ago'
|
||||||
import en from 'javascript-time-ago/locale/en'
|
import en from 'javascript-time-ago/locale/en'
|
||||||
|
|
||||||
@@ -47,3 +47,10 @@ export function formatDateShort(
|
|||||||
|
|
||||||
return transformCase(text, caseType)
|
return transformCase(text, caseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDaysAgo(approvedAt: Date) {
|
||||||
|
const days = differenceInDays(new Date(), approvedAt)
|
||||||
|
if (days === 0) return 'today'
|
||||||
|
if (days === 1) return 'yesterday'
|
||||||
|
return `${days.toLocaleString()} days ago`
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export async function removeUserSessionIdCookie(cookies: AstroCookies) {
|
|||||||
cookies.delete(COOKIE_NAME, { path: '/' })
|
cookies.delete(COOKIE_NAME, { path: '/' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(context: Pick<APIContext, 'cookies' | 'locals'>) {
|
export async function logout(context: Pick<APIContext, 'cookies' | 'locals' | 'request' | 'url'>) {
|
||||||
await stopImpersonating(context)
|
await stopImpersonating(context)
|
||||||
|
|
||||||
await removeUserSessionIdCookie(context.cookies)
|
await removeUserSessionIdCookie(context.cookies)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '../constants/characters'
|
} from '../constants/characters'
|
||||||
|
|
||||||
import { getRandom, typedJoin } from './arrays'
|
import { getRandom, typedJoin } from './arrays'
|
||||||
import { DEPLOYMENT_MODE } from './envVariables'
|
import { DEPLOYMENT_MODE } from './client/envVariables'
|
||||||
import { transformCase } from './strings'
|
import { transformCase } from './strings'
|
||||||
|
|
||||||
const DIGEST = 'sha512'
|
const DIGEST = 'sha512'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LOGS_UI_URL } from 'astro:env/server'
|
|||||||
|
|
||||||
import { SUPPORT_EMAIL } from '../constants/project'
|
import { SUPPORT_EMAIL } from '../constants/project'
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters'
|
import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
---
|
---
|
||||||
layout: ../layouts/MarkdownLayout.astro
|
layout: ../layouts/MarkdownLayout.astro
|
||||||
title: About
|
title: About
|
||||||
author: KYCnot.me
|
updatedAt: 2025-05-15
|
||||||
pubDate: 2025-05-15
|
|
||||||
description: 'Learn how KYCnot.me website works and about our mission to protect privacy in cryptocurrency.'
|
description: 'Learn how KYCnot.me website works and about our mission to protect privacy in cryptocurrency.'
|
||||||
icon: 'ri:information-line'
|
icon: 'ri:information-line'
|
||||||
---
|
---
|
||||||
@@ -166,7 +165,7 @@ The trust score represents how reliable and trustworthy a service is, based on o
|
|||||||
|
|
||||||
##### Overall Score
|
##### Overall Score
|
||||||
|
|
||||||
The overall score is calculated as `(privacy * 0.6) + (trust * 0.4)` and provides a combined measure of privacy and trust.
|
The overall score is calculated as `(privacy * 0.6) + (trust * 0.4)` truncated. This provides a combined measure of privacy and trust.
|
||||||
|
|
||||||
#### Terms of Service Reviews
|
#### Terms of Service Reviews
|
||||||
|
|
||||||
@@ -223,6 +222,10 @@ You can contact via direct chat:
|
|||||||
|
|
||||||
- [SimpleX Chat](https://simplex.chat/contact#/?v=2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FcgKHYUYnpAIVoGb9lxb0qEMEpvYIvc1O%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAIW_JSq8wOsLKG4Xv4O54uT2D_l8MJBYKQIFj1FjZpnU%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion)
|
- [SimpleX Chat](https://simplex.chat/contact#/?v=2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FcgKHYUYnpAIVoGb9lxb0qEMEpvYIvc1O%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAIW_JSq8wOsLKG4Xv4O54uT2D_l8MJBYKQIFj1FjZpnU%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion)
|
||||||
|
|
||||||
|
## Downloads and assets
|
||||||
|
|
||||||
|
For logos and brand assets, visit our [downloads page](/downloads).
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This website is strictly for informational purposes regarding privacy technology in the cryptocurrency space. We unequivocally condemn and do not endorse, support, or facilitate money laundering, terrorist financing, or any other illegal financial activities. The use of any information or service mentioned herein for such purposes is strictly prohibited and contrary to the core principles of this project.
|
This website is strictly for informational purposes regarding privacy technology in the cryptocurrency space. We unequivocally condemn and do not endorse, support, or facilitate money laundering, terrorist financing, or any other illegal financial activities. The use of any information or service mentioned herein for such purposes is strictly prohibited and contrary to the core principles of this project.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks'
|
|||||||
import { prisma } from '../../lib/prisma'
|
import { prisma } from '../../lib/prisma'
|
||||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||||
import { formatDateShort } from '../../lib/timeAgo'
|
import { formatDateShort } from '../../lib/timeAgo'
|
||||||
|
import { urlDomain } from '../../lib/urls'
|
||||||
|
|
||||||
const userId = Astro.locals.user?.id
|
const userId = Astro.locals.user?.id
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -423,7 +424,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-blue-400 hover:underline"
|
class="text-blue-400 hover:underline"
|
||||||
>
|
>
|
||||||
{user.verifiedLink}
|
{urlDomain(user.verifiedLink)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -857,12 +858,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span
|
<span class="border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs">
|
||||||
class={cn(
|
|
||||||
'border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs',
|
|
||||||
statusInfo.iconClass
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ if (toggleResult?.error) {
|
|||||||
label: type.label,
|
label: type.label,
|
||||||
value: type.value,
|
value: type.value,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
noTransitionPersist: false,
|
|
||||||
}))}
|
}))}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
required
|
required
|
||||||
@@ -306,8 +305,8 @@ if (toggleResult?.error) {
|
|||||||
label="Status"
|
label="Status"
|
||||||
error={createInputErrors.isActive}
|
error={createInputErrors.isActive}
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Active', value: 'true', noTransitionPersist: true },
|
{ label: 'Active', value: 'true' },
|
||||||
{ label: 'Inactive', value: 'false', noTransitionPersist: true },
|
{ label: 'Inactive', value: 'false' },
|
||||||
]}
|
]}
|
||||||
selectedValue={newAnnouncement.isActive ? 'true' : 'false'}
|
selectedValue={newAnnouncement.isActive ? 'true' : 'false'}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
@@ -628,7 +627,6 @@ if (toggleResult?.error) {
|
|||||||
label: type.label,
|
label: type.label,
|
||||||
value: type.value,
|
value: type.value,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
required
|
required
|
||||||
@@ -661,8 +659,8 @@ if (toggleResult?.error) {
|
|||||||
name="isActive"
|
name="isActive"
|
||||||
label="Status"
|
label="Status"
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Active', value: 'true', noTransitionPersist: true },
|
{ label: 'Active', value: 'true' },
|
||||||
{ label: 'Inactive', value: 'false', noTransitionPersist: true },
|
{ label: 'Inactive', value: 'false' },
|
||||||
]}
|
]}
|
||||||
selectedValue={announcement.isActive ? 'true' : 'false'}
|
selectedValue={announcement.isActive ? 'true' : 'false'}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
|
|||||||
@@ -692,12 +692,12 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
|
|||||||
|
|
||||||
<div class="flex justify-end space-x-3">
|
<div class="flex justify-end space-x-3">
|
||||||
<Button
|
<Button
|
||||||
as="button"
|
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
size="sm"
|
size="sm"
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
onclick={`document.getElementById('edit-form-${index}').classList.toggle('hidden')`}
|
data-cancel-button
|
||||||
|
data-cancel-form-id={`edit-form-${index}`}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
as="button"
|
as="button"
|
||||||
@@ -721,3 +721,22 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// Optional script for cancel buttons in attribute forms. //
|
||||||
|
// Hides the edit form when cancel button is clicked. //
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>('[data-cancel-button]').forEach((button) => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formId = button.getAttribute('data-cancel-form-id')
|
||||||
|
if (!formId) throw new Error('Form ID not found')
|
||||||
|
const form = document.getElementById(formId)
|
||||||
|
if (!form) throw new Error('Form not found')
|
||||||
|
form.classList.add('hidden')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
verificationStepStatuses,
|
verificationStepStatuses,
|
||||||
} from '../../../../constants/verificationStepStatus'
|
} from '../../../../constants/verificationStepStatus'
|
||||||
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
||||||
import { DEPLOYMENT_MODE } from '../../../../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../../../../lib/client/envVariables'
|
||||||
import { listFiles } from '../../../../lib/fileStorage'
|
import { listFiles } from '../../../../lib/fileStorage'
|
||||||
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
||||||
import { pluralize } from '../../../../lib/pluralize'
|
import { pluralize } from '../../../../lib/pluralize'
|
||||||
@@ -441,7 +441,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
value: kycLevel.id.toString(),
|
value: kycLevel.id.toString(),
|
||||||
icon: kycLevel.icon,
|
icon: kycLevel.icon,
|
||||||
description: kycLevel.description,
|
description: kycLevel.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.kycLevel.toString()}
|
selectedValue={service.kycLevel.toString()}
|
||||||
iconSize="md"
|
iconSize="md"
|
||||||
@@ -458,7 +457,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
value: clarification.value,
|
value: clarification.value,
|
||||||
icon: clarification.icon,
|
icon: clarification.icon,
|
||||||
description: clarification.description,
|
description: clarification.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.kycLevelClarification}
|
selectedValue={service.kycLevelClarification}
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
@@ -475,7 +473,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
icon: status.icon,
|
icon: status.icon,
|
||||||
iconClass: status.classNames.icon,
|
iconClass: status.classNames.icon,
|
||||||
description: status.description,
|
description: status.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.verificationStatus}
|
selectedValue={service.verificationStatus}
|
||||||
error={serviceInputErrors.verificationStatus}
|
error={serviceInputErrors.verificationStatus}
|
||||||
@@ -491,7 +488,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
label: currency.name,
|
label: currency.name,
|
||||||
value: currency.id,
|
value: currency.id,
|
||||||
icon: currency.icon,
|
icon: currency.icon,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.acceptedCurrencies}
|
selectedValue={service.acceptedCurrencies}
|
||||||
error={serviceInputErrors.acceptedCurrencies}
|
error={serviceInputErrors.acceptedCurrencies}
|
||||||
@@ -532,7 +528,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
icon: visibility.icon,
|
icon: visibility.icon,
|
||||||
iconClass: visibility.iconClass,
|
iconClass: visibility.iconClass,
|
||||||
description: visibility.description,
|
description: visibility.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.serviceVisibility}
|
selectedValue={service.serviceVisibility}
|
||||||
error={serviceInputErrors.serviceVisibility}
|
error={serviceInputErrors.serviceVisibility}
|
||||||
|
|||||||
@@ -230,26 +230,22 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
label: 'Admin',
|
label: 'Admin',
|
||||||
value: 'admin',
|
value: 'admin',
|
||||||
icon: 'ri:shield-star-fill',
|
icon: 'ri:shield-star-fill',
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Moderator',
|
label: 'Moderator',
|
||||||
value: 'moderator',
|
value: 'moderator',
|
||||||
icon: 'ri:graduation-cap-fill',
|
icon: 'ri:graduation-cap-fill',
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Spammer',
|
label: 'Spammer',
|
||||||
value: 'spammer',
|
value: 'spammer',
|
||||||
icon: 'ri:alert-fill',
|
icon: 'ri:alert-fill',
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Verified',
|
label: 'Verified',
|
||||||
value: 'verified',
|
value: 'verified',
|
||||||
icon: 'ri:verified-badge-fill',
|
icon: 'ri:verified-badge-fill',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
selectedValue={[
|
selectedValue={[
|
||||||
@@ -434,7 +430,6 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
label: role.label,
|
label: role.label,
|
||||||
value: role.value,
|
value: role.value,
|
||||||
icon: role.icon,
|
icon: role.icon,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
required
|
required
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
|
|||||||
73
web/src/pages/assets.mdx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/MarkdownLayout.astro
|
||||||
|
title: Assets & Downloads
|
||||||
|
author: KYCnot.me
|
||||||
|
pubDate: 2025-06-11
|
||||||
|
description: 'Download KYCnot.me logos and assets.'
|
||||||
|
icon: 'ri:image-line'
|
||||||
|
---
|
||||||
|
|
||||||
|
import PressAssets from '../components/PressAssets.astro'
|
||||||
|
|
||||||
|
Please, link back to [KYCnot.me](https://kycnot.me) when possible, and use responsibly.
|
||||||
|
|
||||||
|
<PressAssets
|
||||||
|
title="KYCnot.me Brand Kit"
|
||||||
|
description="Complete collection of our logos and 'Review on KYCnot.me' badges in various formats and styles."
|
||||||
|
zipPath="/kycnotme-press-assets.zip"
|
||||||
|
assets={[
|
||||||
|
{
|
||||||
|
name: 'Logo - Normal',
|
||||||
|
path: '/logo/logo-normal.svg',
|
||||||
|
alt: 'KYCnot.me logo normal version',
|
||||||
|
category: 'logo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logo - Small',
|
||||||
|
path: '/logo/logo-small.svg',
|
||||||
|
alt: 'KYCnot.me logo small version',
|
||||||
|
category: 'logo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logo - Mini',
|
||||||
|
path: '/logo/logo-mini.svg',
|
||||||
|
alt: 'KYCnot.me logo mini version',
|
||||||
|
category: 'logo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logo - Mini Full',
|
||||||
|
path: '/logo/logo-mini-full.svg',
|
||||||
|
alt: 'KYCnot.me logo mini full version',
|
||||||
|
category: 'logo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Badge - Long Black',
|
||||||
|
path: '/review-on-kycnotme/long-black.svg',
|
||||||
|
alt: 'Review on KYCnot.me badge - long black version',
|
||||||
|
category: 'badge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Badge - Long White',
|
||||||
|
path: '/review-on-kycnotme/long-white.svg',
|
||||||
|
alt: 'Review on KYCnot.me badge - long white version',
|
||||||
|
category: 'badge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Badge - Short Black',
|
||||||
|
path: '/review-on-kycnotme/short-black.svg',
|
||||||
|
alt: 'Review on KYCnot.me badge - short black version',
|
||||||
|
category: 'badge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Badge - Short White',
|
||||||
|
path: '/review-on-kycnotme/short-white.svg',
|
||||||
|
alt: 'Review on KYCnot.me badge - short white version',
|
||||||
|
category: 'badge',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Brand design
|
||||||
|
|
||||||
|
- Brand color: `#3bdb78`
|
||||||
|
- Font: [Space Grotesk](https://floriankarsten.github.io/space-grotesk/) and [Inter](https://rsms.me/inter/).
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
layout: ../../layouts/MarkdownLayout.astro
|
layout: ../../layouts/MarkdownLayout.astro
|
||||||
title: API
|
title: API
|
||||||
author: KYCnot.me
|
updatedAt: 2025-05-31
|
||||||
pubDate: 2025-05-31
|
|
||||||
description: 'Access basic service data via our public API.'
|
description: 'Access basic service data via our public API.'
|
||||||
icon: 'ri:plug-line'
|
icon: 'ri:plug-line'
|
||||||
---
|
---
|
||||||
|
|
||||||
import { SOURCE_CODE_URL } from 'astro:env/server'
|
import { SOURCE_CODE_URL } from 'astro:env/client'
|
||||||
import { kycLevels } from '../../constants/kycLevels'
|
import { kycLevels } from '../../constants/kycLevels'
|
||||||
import { verificationStatuses } from '../../constants/verificationStatus'
|
import { verificationStatuses } from '../../constants/verificationStatus'
|
||||||
import { serviceVisibilities } from '../../constants/serviceVisibility'
|
import { serviceVisibilities } from '../../constants/serviceVisibility'
|
||||||
@@ -53,6 +52,7 @@ type ServiceResponse = {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
verifiedAt: Date | null
|
verifiedAt: Date | null
|
||||||
|
approvedAt: Date | null
|
||||||
kycLevel: 0 | 1 | 2 | 3 | 4
|
kycLevel: 0 | 1 | 2 | 3 | 4
|
||||||
kycLevelInfo: {
|
kycLevelInfo: {
|
||||||
value: 0 | 1 | 2 | 3 | 4
|
value: 0 | 1 | 2 | 3 | 4
|
||||||
@@ -146,7 +146,8 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
|||||||
"labelShort": "Verified",
|
"labelShort": "Verified",
|
||||||
"description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee."
|
"description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee."
|
||||||
},
|
},
|
||||||
"verifiedAt": "2025-01-20T07:12:29.393Z",
|
"verifiedAt": "2025-06-14T11:02:39.294Z",
|
||||||
|
"approvedAt": "2025-05-31T19:09:18.043Z",
|
||||||
"kycLevel": 0,
|
"kycLevel": 0,
|
||||||
"kycLevelInfo": {
|
"kycLevelInfo": {
|
||||||
"value": 0,
|
"value": 0,
|
||||||
@@ -164,7 +165,7 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
|||||||
"slug": "exchange"
|
"slug": "exchange"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"listedAt": "2025-05-31T19:09:18.043Z",
|
"listedAt": "2025-04-20T07:12:29.393Z",
|
||||||
"serviceUrls": [
|
"serviceUrls": [
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
"http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion"
|
"http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion"
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ layout: ../../layouts/MarkdownLayout.astro
|
|||||||
title: How does karma work?
|
title: How does karma work?
|
||||||
description: "KYCnot.me has a user karma system, here's how it works"
|
description: "KYCnot.me has a user karma system, here's how it works"
|
||||||
icon: 'ri:hearts-line'
|
icon: 'ri:hearts-line'
|
||||||
author: KYCnot.me
|
updatedAt: 2025-05-15
|
||||||
pubDate: 2025-05-15
|
|
||||||
---
|
---
|
||||||
|
|
||||||
import KarmaUnlocksTable from '../../components/KarmaUnlocksTable.astro'
|
import KarmaUnlocksTable from '../../components/KarmaUnlocksTable.astro'
|
||||||
|
|||||||
21
web/src/pages/downloads.mdx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/MarkdownLayout.astro
|
||||||
|
title: Downloads and assets
|
||||||
|
author: KYCnot.me
|
||||||
|
updatedAt: 2025-06-12
|
||||||
|
description: 'Download KYCnot.me logos and assets.'
|
||||||
|
icon: 'ri:image-line'
|
||||||
|
---
|
||||||
|
|
||||||
|
import PressAssets from '../components/PressAssets.astro'
|
||||||
|
|
||||||
|
Please, link back to [KYCnot.me](https://kycnot.me) when possible, and use responsibly.
|
||||||
|
|
||||||
|
<PressAssets />
|
||||||
|
|
||||||
|
Review service link format: `https://kycnot.me/service/[slug]/review`
|
||||||
|
|
||||||
|
## Brand design
|
||||||
|
|
||||||
|
- Brand color: `#3bdb78`
|
||||||
|
- Font: [Space Grotesk](https://floriankarsten.github.io/space-grotesk/) and [Inter](https://rsms.me/inter/).
|
||||||
@@ -45,7 +45,6 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
async () =>
|
async () =>
|
||||||
prisma.service.findMany({
|
prisma.service.findMany({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -72,7 +71,6 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
},
|
},
|
||||||
service: {
|
service: {
|
||||||
slug: params.service ?? undefined,
|
slug: params.service ?? undefined,
|
||||||
listedAt: params.service ? undefined : { lte: new Date() },
|
|
||||||
serviceVisibility: {
|
serviceVisibility: {
|
||||||
in: params.service ? ['PUBLIC', 'ARCHIVED', 'UNLISTED'] : ['PUBLIC', 'ARCHIVED'],
|
in: params.service ? ['PUBLIC', 'ARCHIVED', 'UNLISTED'] : ['PUBLIC', 'ARCHIVED'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ const servicesQMatch = filters.q ? await findServicesBySimilarity(filters.q) : n
|
|||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
id: servicesQMatch ? { in: servicesQMatch.map(({ id }) => id) } : undefined,
|
id: servicesQMatch ? { in: servicesQMatch.map(({ id }) => id) } : undefined,
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
categories: filters.categories.length ? { some: { slug: { in: filters.categories } } } : undefined,
|
categories: filters.categories.length ? { some: { slug: { in: filters.categories } } } : undefined,
|
||||||
verificationStatus: {
|
verificationStatus: {
|
||||||
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
||||||
@@ -317,7 +316,6 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
|||||||
services: {
|
services: {
|
||||||
where: {
|
where: {
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ const [dbNotifications, notificationPreferences, totalNotifications, pushSubscri
|
|||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
select: {
|
select: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
userAgent: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -93,6 +92,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
referral: true,
|
referral: true,
|
||||||
imageUrl: true,
|
imageUrl: true,
|
||||||
listedAt: true,
|
listedAt: true,
|
||||||
|
approvedAt: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
acceptedCurrencies: true,
|
acceptedCurrencies: true,
|
||||||
tosReview: true,
|
tosReview: true,
|
||||||
@@ -100,7 +100,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
userSentiment: true,
|
userSentiment: true,
|
||||||
userSentimentAt: true,
|
userSentimentAt: true,
|
||||||
averageUserRating: true,
|
averageUserRating: true,
|
||||||
isRecentlyListed: true,
|
isRecentlyApproved: true,
|
||||||
contactMethods: {
|
contactMethods: {
|
||||||
select: {
|
select: {
|
||||||
value: true,
|
value: true,
|
||||||
@@ -230,7 +230,6 @@ if (!service) {
|
|||||||
where: {
|
where: {
|
||||||
previousSlugs: { has: slug },
|
previousSlugs: { has: slug },
|
||||||
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
},
|
},
|
||||||
select: { slug: true },
|
select: { slug: true },
|
||||||
})
|
})
|
||||||
@@ -294,6 +293,8 @@ const statusIcon = {
|
|||||||
APPROVED: undefined,
|
APPROVED: undefined,
|
||||||
}[service.verificationStatus]
|
}[service.verificationStatus]
|
||||||
|
|
||||||
|
const isScam = service.verificationStatus === 'VERIFICATION_FAILED'
|
||||||
|
|
||||||
const shuffledLinks = {
|
const shuffledLinks = {
|
||||||
clearnet: shuffle(service.serviceUrls),
|
clearnet: shuffle(service.serviceUrls),
|
||||||
onion: shuffle(service.onionUrls),
|
onion: shuffle(service.onionUrls),
|
||||||
@@ -385,6 +386,13 @@ const getVerificationStepStatusInfo = (status: VerificationStepStatus) => {
|
|||||||
color: 'red',
|
color: 'red',
|
||||||
timelineIconClass: 'text-red-400',
|
timelineIconClass: 'text-red-400',
|
||||||
} as const
|
} as const
|
||||||
|
case VerificationStepStatus.WARNING:
|
||||||
|
return {
|
||||||
|
text: 'Warning',
|
||||||
|
icon: 'ri:alert-line',
|
||||||
|
color: 'yellow',
|
||||||
|
timelineIconClass: 'text-yellow-400',
|
||||||
|
} as const
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
text: 'Unknown',
|
text: 'Unknown',
|
||||||
@@ -407,9 +415,12 @@ const ogImageTemplateData = {
|
|||||||
|
|
||||||
const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility)
|
const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility)
|
||||||
|
|
||||||
const activeAlertOrWarningEvents = service.events.filter(
|
const activeAlertOrWarningEvents = service.events
|
||||||
(event) => getEventTypeInfo(event.type).showBanner && (event.endedAt === null || event.endedAt >= now)
|
.map((event) => ({
|
||||||
)
|
...event,
|
||||||
|
typeInfo: getEventTypeInfo(event.type),
|
||||||
|
}))
|
||||||
|
.filter((event) => event.typeInfo.showBanner && (event.endedAt === null || event.endedAt >= now))
|
||||||
const activeEventToShow =
|
const activeEventToShow =
|
||||||
activeAlertOrWarningEvents.find((event) => event.type === EventType.ALERT) ?? activeAlertOrWarningEvents[0]
|
activeAlertOrWarningEvents.find((event) => event.type === EventType.ALERT) ?? activeAlertOrWarningEvents[0]
|
||||||
---
|
---
|
||||||
@@ -518,15 +529,10 @@ const activeEventToShow =
|
|||||||
href="#events"
|
href="#events"
|
||||||
class={cn(
|
class={cn(
|
||||||
'group mb-4 block rounded-md px-3 py-2 text-sm transition-colors duration-200',
|
'group mb-4 block rounded-md px-3 py-2 text-sm transition-colors duration-200',
|
||||||
activeEventToShow.type === EventType.ALERT
|
activeEventToShow.typeInfo.classNames.banner
|
||||||
? 'bg-red-900/50 text-red-300 hover:bg-red-800/60 focus-visible:bg-red-800/60'
|
|
||||||
: 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60 focus-visible:bg-yellow-800/60'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon name={activeEventToShow.typeInfo.icon} class="me-1.5 inline-block size-4 align-[-0.15em]" />
|
||||||
name={activeEventToShow.type === EventType.ALERT ? 'ri:alert-fill' : 'ri:alarm-warning-fill'}
|
|
||||||
class="me-1.5 inline-block size-4 align-[-0.15em]"
|
|
||||||
/>
|
|
||||||
<span class="font-bold">{activeEventToShow.title}</span> — {activeEventToShow.content}
|
<span class="font-bold">{activeEventToShow.title}</span> — {activeEventToShow.content}
|
||||||
{activeAlertOrWarningEvents.length >= 2 && <>+{activeAlertOrWarningEvents.length - 1} more events.</>}
|
{activeAlertOrWarningEvents.length >= 2 && <>+{activeAlertOrWarningEvents.length - 1} more events.</>}
|
||||||
<span class="underline">Go to events</span>
|
<span class="underline">Go to events</span>
|
||||||
@@ -758,11 +764,18 @@ const activeEventToShow =
|
|||||||
<ul aria-label="Service links" class="xs:justify-start mt-4 flex flex-wrap justify-center gap-2">
|
<ul aria-label="Service links" class="xs:justify-start mt-4 flex flex-wrap justify-center gap-2">
|
||||||
{shownLinks.map((url) => (
|
{shownLinks.map((url) => (
|
||||||
<li>
|
<li>
|
||||||
<ServiceLinkButton
|
{isScam ? (
|
||||||
url={url}
|
<span class="2xs:text-sm 2xs:h-8 2xs:gap-2 2xs:px-4 bg-day-800 inline-flex h-6 items-center gap-1 rounded-full px-2 text-xs whitespace-nowrap text-red-400">
|
||||||
referral={service.referral}
|
<Icon name="ri:alert-line" class="size-4 text-red-400" />
|
||||||
enableMinWidth={shuffledLinks.onion.length + shuffledLinks.i2p.length > 0}
|
{urlDomain(url)}
|
||||||
/>
|
</span>
|
||||||
|
) : (
|
||||||
|
<ServiceLinkButton
|
||||||
|
url={url}
|
||||||
|
referral={service.referral}
|
||||||
|
enableMinWidth={shuffledLinks.onion.length + shuffledLinks.i2p.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -786,11 +799,18 @@ const activeEventToShow =
|
|||||||
|
|
||||||
{hiddenLinks.map((url) => (
|
{hiddenLinks.map((url) => (
|
||||||
<li class="hidden peer-checked:block">
|
<li class="hidden peer-checked:block">
|
||||||
<ServiceLinkButton
|
{isScam ? (
|
||||||
url={url}
|
<span class="2xs:text-sm 2xs:h-8 2xs:gap-2 2xs:px-4 bg-day-800 inline-flex h-6 items-center gap-1 rounded-full px-2 text-xs whitespace-nowrap text-red-400">
|
||||||
referral={service.referral}
|
<Icon name="ri:alert-line" class="size-4 text-red-400" />
|
||||||
enableMinWidth={shuffledLinks.onion.length + shuffledLinks.i2p.length > 0}
|
{urlDomain(url)}
|
||||||
/>
|
</span>
|
||||||
|
) : (
|
||||||
|
<ServiceLinkButton
|
||||||
|
url={url}
|
||||||
|
referral={service.referral}
|
||||||
|
enableMinWidth={shuffledLinks.onion.length + shuffledLinks.i2p.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -984,15 +1004,23 @@ const activeEventToShow =
|
|||||||
attribute.typeInfo.classNames.text
|
attribute.typeInfo.classNames.text
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<span class="mr-auto ml-1">{attribute.title}</span>
|
||||||
name={attribute.categoryInfo.icon}
|
<>
|
||||||
class={cn('mr-2 size-4 flex-shrink-0', attribute.typeInfo.classNames.icon)}
|
{weights.map((w) => (
|
||||||
/>
|
<span
|
||||||
<span>{attribute.title}</span>
|
class={cn(
|
||||||
|
'ml-2 text-center text-sm',
|
||||||
|
w.value === 0 ? 'text-current/30' : 'text-current/60'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatNumber(w.value, w.formatOptions)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
<Icon
|
<Icon
|
||||||
name="ri:arrow-down-s-line"
|
name="ri:arrow-down-s-line"
|
||||||
class={cn(
|
class={cn(
|
||||||
'ml-auto size-5 group-open/attribute:rotate-180',
|
'ml-1 size-5 group-open/attribute:rotate-180',
|
||||||
attribute.typeInfo.classNames.icon
|
attribute.typeInfo.classNames.icon
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -1059,17 +1087,17 @@ const activeEventToShow =
|
|||||||
baseScoreType.classNames.text
|
baseScoreType.classNames.text
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<span class="font-title mr-auto ml-1">{baseScoreType.label}</span>
|
||||||
name={baseScoreType.icon}
|
<span class="mr-2 text-current/60">+50</span>
|
||||||
class={cn('mr-2 size-4 flex-shrink-0', baseScoreType.classNames.icon)}
|
<span class="mr-1 text-current/60">+50</span>
|
||||||
/>
|
|
||||||
<span class="font-title">{baseScoreType.label}</span>
|
|
||||||
<span class={cn('mr-1 ml-auto', baseScoreType.classNames.icon)}>+50</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<p class="text-day-400 mt-3 text-center text-xs">
|
||||||
|
<span class="hover:text-day-200 transition-colors">Overall = 60% Privacy + 40% Trust (Truncated)</span>
|
||||||
|
</p>
|
||||||
<div class="xs:gap-x-6 mt-2 flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs">
|
<div class="xs:gap-x-6 mt-2 flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs">
|
||||||
<a
|
<a
|
||||||
href="/about#service-scores"
|
href="/about#service-scores"
|
||||||
@@ -1083,7 +1111,7 @@ const activeEventToShow =
|
|||||||
class="text-day-400 hover:text-day-200 inline-flex items-center gap-1 transition-colors hover:underline"
|
class="text-day-400 hover:text-day-200 inline-flex items-center gap-1 transition-colors hover:underline"
|
||||||
>
|
>
|
||||||
<Icon name="ri:information-line" class="size-3" />
|
<Icon name="ri:information-line" class="size-3" />
|
||||||
Attributes list
|
All attributes list
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1194,15 +1222,15 @@ const activeEventToShow =
|
|||||||
<p class="text-day-500 mt-1 text-sm text-balance">
|
<p class="text-day-500 mt-1 text-sm text-balance">
|
||||||
Maybe due to captchas, client side rendering, DDoS protections, or non-text format.
|
Maybe due to captchas, client side rendering, DDoS protections, or non-text format.
|
||||||
</p>
|
</p>
|
||||||
{service.tosUrls.length > 0 && (
|
<p class="mt-2 text-xs">
|
||||||
<p class="mt-2 text-xs">
|
Reviewed <TimeFormatted date={service.tosReviewAt} hourPrecision />
|
||||||
{service.tosUrls.map((url) => (
|
{service.tosUrls.length > 0 && 'from'}
|
||||||
<a href={url} class="hover:underline">
|
{service.tosUrls.map((url) => (
|
||||||
{urlDomain(url)}
|
<a href={url} class="hover:underline">
|
||||||
</a>
|
{urlDomain(url)}
|
||||||
))}
|
</a>
|
||||||
</p>
|
))}
|
||||||
)}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks'
|
|||||||
import { prisma } from '../../lib/prisma'
|
import { prisma } from '../../lib/prisma'
|
||||||
import { KYCNOTME_SCHEMA_MINI } from '../../lib/schema'
|
import { KYCNOTME_SCHEMA_MINI } from '../../lib/schema'
|
||||||
import { formatDateShort } from '../../lib/timeAgo'
|
import { formatDateShort } from '../../lib/timeAgo'
|
||||||
|
import { urlDomain } from '../../lib/urls'
|
||||||
|
|
||||||
import type { ProfilePage, WithContext } from 'schema-dts'
|
import type { ProfilePage, WithContext } from 'schema-dts'
|
||||||
|
|
||||||
@@ -96,9 +97,6 @@ const user = await Astro.locals.banners.try('user', async () => {
|
|||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
service: {
|
service: {
|
||||||
listedAt: {
|
|
||||||
lte: new Date(),
|
|
||||||
},
|
|
||||||
serviceVisibility: {
|
serviceVisibility: {
|
||||||
in: ['PUBLIC', 'ARCHIVED'],
|
in: ['PUBLIC', 'ARCHIVED'],
|
||||||
},
|
},
|
||||||
@@ -511,7 +509,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-blue-400 hover:underline"
|
class="text-blue-400 hover:underline"
|
||||||
>
|
>
|
||||||
{user.verifiedLink}
|
{urlDomain(user.verifiedLink)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -975,12 +973,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span
|
<span class="border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs">
|
||||||
class={cn(
|
|
||||||
'border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs',
|
|
||||||
statusInfo.iconClass
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -103,10 +103,6 @@
|
|||||||
drop-shadow(0 0 4px color-mix(in oklab, currentColor 60%, transparent));
|
drop-shadow(0 0 4px color-mix(in oklab, currentColor 60%, transparent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility scrollbar-w-none {
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility checkbox-force-checked {
|
@utility checkbox-force-checked {
|
||||||
&:not(:checked) {
|
&:not(:checked) {
|
||||||
@apply border-transparent! bg-current/50!;
|
@apply border-transparent! bg-current/50!;
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
import { clientsClaim } from 'workbox-core'
|
import { clientsClaim } from 'workbox-core'
|
||||||
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
||||||
|
|
||||||
import { makeNotificationOptions } from './lib/notificationOptions'
|
import {
|
||||||
|
makeBrowserNotificationOptions,
|
||||||
|
makeBrowserNotificationTitle,
|
||||||
|
} from './lib/client/notificationOptions'
|
||||||
|
|
||||||
import type { NotificationData, NotificationPayload } from './lib/serverEventsTypes'
|
import type { NotificationData, NotificationPayload } from './lib/serverEventsTypes'
|
||||||
|
|
||||||
@@ -59,8 +62,8 @@ async function handleNotificationClick(url: string) {
|
|||||||
|
|
||||||
async function showPushNotification(payload: NotificationPayload | null) {
|
async function showPushNotification(payload: NotificationPayload | null) {
|
||||||
await self.registration.showNotification(
|
await self.registration.showNotification(
|
||||||
payload?.title ?? 'New Notification',
|
makeBrowserNotificationTitle(payload?.title),
|
||||||
makeNotificationOptions(payload)
|
makeBrowserNotificationOptions(payload)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||