From 812937d2c766f609789c66dffb8ab4f905bf7e1d Mon Sep 17 00:00:00 2001 From: pluja Date: Tue, 10 Jun 2025 17:42:42 +0000 Subject: [PATCH] Release 202506101742 --- pyworker/pyworker/database.py | 6 +- pyworker/pyworker/tasks/tos_review.py | 106 ++--- pyworker/pyworker/utils/ai.py | 2 +- web/package-lock.json | 41 ++ web/package.json | 1 + .../migration.sql | 2 + .../migration.sql | 11 + .../migration.sql | 8 + web/prisma/schema.prisma | 2 + web/prisma/triggers/02_service_score.sql | 16 +- .../07_notifications_service_suggestion.sql | 22 +- web/src/actions/serviceSuggestion.ts | 35 +- web/src/components/BaseHead.astro | 17 +- web/src/components/DynamicFavicon.astro | 2 +- web/src/components/Footer.astro | 15 +- .../HeaderNotificationIndicator.astro | 2 +- web/src/components/InputCheckbox.astro | 61 +++ web/src/components/InputTextArea.astro | 1 + .../components/NotificationEventsScript.astro | 2 +- web/src/components/ServerEventsScript.astro | 2 +- web/src/components/ServiceWorkerScript.astro | 54 +++ web/src/constants/notificationTypes.ts | 5 + web/src/constants/serviceSuggestionType.ts | 4 + web/src/layouts/BaseLayout.astro | 15 +- web/src/layouts/MiniLayout.astro | 34 +- web/src/lib/attributes.ts | 361 ++++++++++++------ web/src/lib/feeds.ts | 323 ++++++++++++++++ web/src/lib/notifications.ts | 20 + web/src/lib/sendNotifications.ts | 1 + web/src/lib/serverEventsTypes.ts | 2 +- web/src/lib/zodUtils.ts | 4 +- web/src/pages/404.astro | 3 +- web/src/pages/500.astro | 18 +- web/src/pages/access-denied.astro | 2 +- web/src/pages/account/index.astro | 2 +- web/src/pages/admin/attributes.astro | 50 +-- web/src/pages/admin/releases.astro | 5 +- .../pages/admin/services/[slug]/edit.astro | 2 +- web/src/pages/admin/users/[username].astro | 2 +- web/src/pages/attributes.astro | 20 +- web/src/pages/events.astro | 7 +- web/src/pages/feeds/events.xml.ts | 41 ++ web/src/pages/feeds/index.astro | 87 +++++ .../feeds/service/[slug]/comments.xml.ts | 61 +++ .../pages/feeds/service/[slug]/events.xml.ts | 41 ++ .../feeds/user/[feedId]/notifications.xml.ts | 52 +++ web/src/pages/notifications.astro | 68 +++- web/src/pages/service-suggestion/new.astro | 21 +- web/src/pages/u/[username].astro | 2 +- web/src/pwa.ts | 21 - 50 files changed, 1347 insertions(+), 335 deletions(-) create mode 100644 web/prisma/migrations/20250609213656_suggestion_created_notification_type/migration.sql create mode 100644 web/prisma/migrations/20250610163329_feedi_optional/migration.sql create mode 100644 web/prisma/migrations/20250610171300_required_feed_id/migration.sql create mode 100644 web/src/components/InputCheckbox.astro create mode 100644 web/src/components/ServiceWorkerScript.astro create mode 100644 web/src/lib/feeds.ts create mode 100644 web/src/pages/feeds/events.xml.ts create mode 100644 web/src/pages/feeds/index.astro create mode 100644 web/src/pages/feeds/service/[slug]/comments.xml.ts create mode 100644 web/src/pages/feeds/service/[slug]/events.xml.ts create mode 100644 web/src/pages/feeds/user/[feedId]/notifications.xml.ts delete mode 100644 web/src/pwa.ts diff --git a/pyworker/pyworker/database.py b/pyworker/pyworker/database.py index ab18eaf..597914a 100644 --- a/pyworker/pyworker/database.py +++ b/pyworker/pyworker/database.py @@ -332,7 +332,7 @@ def remove_service_attribute_by_slug(service_id: int, attribute_slug: str) -> bo return remove_service_attribute(service_id, attribute_id) -def save_tos_review(service_id: int, review: TosReviewType): +def save_tos_review(service_id: int, review: Optional[TosReviewType]): """ Save a TOS review for a specific service. @@ -341,8 +341,8 @@ def save_tos_review(service_id: int, review: TosReviewType): review: A TypedDict containing the review data. """ try: - # Serialize the dictionary to a JSON string for the database - review_json = json.dumps(review) + # 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 conn.cursor(row_factory=dict_row) as cursor: cursor.execute( diff --git a/pyworker/pyworker/tasks/tos_review.py b/pyworker/pyworker/tasks/tos_review.py index c5d8da9..578d236 100644 --- a/pyworker/pyworker/tasks/tos_review.py +++ b/pyworker/pyworker/tasks/tos_review.py @@ -3,7 +3,7 @@ Task for retrieving Terms of Service (TOS) text. """ import hashlib -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Literal from pyworker.database import TosReviewType, save_tos_review, update_kyc_level from pyworker.tasks.base import Task @@ -52,65 +52,71 @@ class TosReviewTask(Task): ) self.logger.info(f"TOS URLs: {tos_urls}") + review = self.get_tos_review(tos_urls, service.get("tosReview")) + save_tos_review(service_id, review) + + # Update the KYC level based on the review + if "kycLevel" in review: + kyc_level = review["kycLevel"] + self.logger.info(f"Updating KYC level to {kyc_level} for service {service_name}") + update_kyc_level(service_id, kyc_level) + + return review + + def get_tos_review( + self, tos_urls: list[str], current_review: Optional[TosReviewType] + ) -> Optional[TosReviewType]: + """ + Get TOS review from a list of URLs. + + Args: + tos_urls: List of TOS URLs to check + current_review: Current review data from the database + + Returns: + Dict containing: + - status: Literal["skipped", "failed", "success"] + - review: Optional[TosReviewType] - The review data if successful + """ + all_skipped = True + for tos_url in tos_urls: api_url = f"{tos_url}" self.logger.info(f"Fetching TOS from URL: {api_url}") - # Sleep for 1 second to avoid rate limiting content = fetch_markdown(api_url) - if content: - # Hash the content to avoid repeating the same content - content_hash = hashlib.sha256(content.encode()).hexdigest() - self.logger.info(f"Content hash: {content_hash}") + if not content: + self.logger.warning(f"Failed to retrieve TOS content for URL: {tos_url}") + all_skipped = False + continue - # service.get("tosReview") can be None if the DB field is NULL. - # Default to an empty dict to prevent AttributeError on .get() - tos_review_data_from_service: Optional[Dict[str, Any]] = service.get( - "tosReview" - ) - tos_review: Dict[str, Any] = ( - tos_review_data_from_service - if tos_review_data_from_service is not None - else {} + # Hash the content to avoid repeating the same content + content_hash = hashlib.sha256(content.encode()).hexdigest() + self.logger.info(f"Content hash: {content_hash}") + + # Skip processing if we've seen this content before + if current_review and current_review.get("contentHash") == content_hash: + self.logger.info( + f"Skipping already processed TOS content with hash: {content_hash}" ) + continue - stored_hash = tos_review.get("contentHash") + all_skipped = False - # Skip processing if we've seen this content before - if stored_hash == content_hash: - self.logger.info( - f"Skipping already processed TOS content with hash: {content_hash}" - ) - continue + # Skip incomplete TOS content + check = prompt_check_tos_review(content) + if not check or not check["isComplete"]: + continue - # Skip incomplete TOS content - check = prompt_check_tos_review(content) - if not check: - continue - elif not check["isComplete"]: - continue - - # Query OpenAI to summarize the content - review = prompt_tos_review(content) - - if review: - review["contentHash"] = content_hash - # Save the review to the database - save_tos_review(service_id, review) - - # Update the KYC level based on the review - if "kycLevel" in review: - kyc_level = review["kycLevel"] - self.logger.info( - f"Updating KYC level to {kyc_level} for service {service_name}" - ) - update_kyc_level(service_id, kyc_level) - # no need to check other TOS URLs - break + # Query OpenAI to summarize the content + review = prompt_tos_review(content) + if review: + review["contentHash"] = content_hash return review - else: - self.logger.warning( - f"Failed to retrieve TOS content for URL: {tos_url}" - ) + + if all_skipped: + return current_review + + return None diff --git a/pyworker/pyworker/utils/ai.py b/pyworker/pyworker/utils/ai.py index aa98768..aed3d51 100644 --- a/pyworker/pyworker/utils/ai.py +++ b/pyworker/pyworker/utils/ai.py @@ -178,7 +178,7 @@ type TosReview = { The rating is a number between 0 and 2, where 0 is informative, 1 is warning, and 2 is critical. -Be concise but thorough, and make sure your output is properly formatted JSON. +Do not provide more than 8 highlights. Focus on the most important information for the user. Be concise but thorough, and make sure your output is properly formatted JSON. """ PROMPT_COMMENT_SENTIMENT_SUMMARY = """ diff --git a/web/package-lock.json b/web/package-lock.json index 7a226d9..489ec27 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,6 +12,7 @@ "@astrojs/db": "0.15.0", "@astrojs/mdx": "4.3.0", "@astrojs/node": "9.2.2", + "@astrojs/rss": "4.0.12", "@astrojs/sitemap": "3.4.1", "@fontsource-variable/space-grotesk": "5.2.8", "@fontsource/inter": "5.2.5", @@ -320,6 +321,16 @@ "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, + "node_modules/@astrojs/rss": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.12.tgz", + "integrity": "sha512-O5yyxHuDVb6DQ6VLOrbUVFSm+NpObulPxjs6XT9q3tC+RoKbN4HXMZLpv0LvXd1qdAjzVgJ1NFD+zKHJNDXikw==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.2.0", + "kleur": "^4.1.5" + } + }, "node_modules/@astrojs/sitemap": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.4.1.tgz", @@ -9789,6 +9800,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -16567,6 +16596,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", diff --git a/web/package.json b/web/package.json index 27c0702..1121d46 100644 --- a/web/package.json +++ b/web/package.json @@ -28,6 +28,7 @@ "@astrojs/db": "0.15.0", "@astrojs/mdx": "4.3.0", "@astrojs/node": "9.2.2", + "@astrojs/rss": "4.0.12", "@astrojs/sitemap": "3.4.1", "@fontsource-variable/space-grotesk": "5.2.8", "@fontsource/inter": "5.2.5", diff --git a/web/prisma/migrations/20250609213656_suggestion_created_notification_type/migration.sql b/web/prisma/migrations/20250609213656_suggestion_created_notification_type/migration.sql new file mode 100644 index 0000000..8e5a6e3 --- /dev/null +++ b/web/prisma/migrations/20250609213656_suggestion_created_notification_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "NotificationType" ADD VALUE 'SUGGESTION_CREATED'; diff --git a/web/prisma/migrations/20250610163329_feedi_optional/migration.sql b/web/prisma/migrations/20250610163329_feedi_optional/migration.sql new file mode 100644 index 0000000..871cff8 --- /dev/null +++ b/web/prisma/migrations/20250610163329_feedi_optional/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[feedId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "feedId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "User_feedId_key" ON "User"("feedId"); diff --git a/web/prisma/migrations/20250610171300_required_feed_id/migration.sql b/web/prisma/migrations/20250610171300_required_feed_id/migration.sql new file mode 100644 index 0000000..f4d3e47 --- /dev/null +++ b/web/prisma/migrations/20250610171300_required_feed_id/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `feedId` on table `User` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "feedId" SET NOT NULL; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index 32d286a..a041a11 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -135,6 +135,7 @@ enum NotificationType { COMMUNITY_NOTE_ADDED /// Comment that is not a reply. May include a rating. ROOT_COMMENT_CREATED + SUGGESTION_CREATED SUGGESTION_MESSAGE SUGGESTION_STATUS_CHANGE // KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. @@ -497,6 +498,7 @@ model User { moderator Boolean @default(false) verifiedLink String? secretTokenHash String @unique + feedId String @unique @default(cuid(2)) /// Computed via trigger. Do not update through prisma. totalKarma Int @default(0) diff --git a/web/prisma/triggers/02_service_score.sql b/web/prisma/triggers/02_service_score.sql index 6c21d3c..80355f6 100644 --- a/web/prisma/triggers/02_service_score.sql +++ b/web/prisma/triggers/02_service_score.sql @@ -22,7 +22,7 @@ DROP FUNCTION IF EXISTS recalculate_scores_for_attribute(); CREATE OR REPLACE FUNCTION calculate_privacy_score(service_id INT) RETURNS INT AS $$ DECLARE - privacy_score INT := 50; -- Start from middle value (50) + privacy_score INT := 0; kyc_factor INT; onion_factor INT := 0; i2p_factor INT := 0; @@ -78,7 +78,7 @@ BEGIN WHERE sa."serviceId" = service_id AND a."category" = 'PRIVACY'; -- Calculate final privacy score (base 100) - privacy_score := privacy_score + kyc_factor + onion_factor + i2p_factor + monero_factor + open_source_factor + p2p_factor + decentralized_factor + attributes_score; + privacy_score := 50 + kyc_factor + onion_factor + i2p_factor + monero_factor + open_source_factor + p2p_factor + decentralized_factor + attributes_score; -- Ensure the score is in reasonable bounds (0-100) privacy_score := GREATEST(0, LEAST(100, privacy_score)); @@ -91,9 +91,11 @@ $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION calculate_trust_score(service_id INT) RETURNS INT AS $$ DECLARE - trust_score INT := 50; -- Start from middle value (50) + trust_score INT := 0; verification_factor INT; attributes_score INT := 0; + recently_listed_factor INT := 0; + tos_penalty_factor INT := 0; BEGIN -- Get verification status factor SELECT @@ -124,7 +126,7 @@ BEGIN AND "verificationStatus" = 'APPROVED' AND (NOW() - "listedAt") <= INTERVAL '15 days' ) THEN - trust_score := trust_score - 10; + recently_listed_factor := -10; -- Update the isRecentlyListed flag to true UPDATE "Service" SET "isRecentlyListed" = TRUE @@ -144,12 +146,12 @@ BEGIN AND "tosReviewAt" IS NOT NULL AND "tosReview" IS NULL ) THEN - trust_score := trust_score - 3; + tos_penalty_factor := -3; END IF; -- Calculate final trust score (base 100) - trust_score := trust_score + verification_factor + attributes_score; - + trust_score := 50 + verification_factor + attributes_score + recently_listed_factor + tos_penalty_factor; + -- Ensure the score is in reasonable bounds (0-100) trust_score := GREATEST(0, LEAST(100, trust_score)); diff --git a/web/prisma/triggers/07_notifications_service_suggestion.sql b/web/prisma/triggers/07_notifications_service_suggestion.sql index 4da39fa..04d4477 100644 --- a/web/prisma/triggers/07_notifications_service_suggestion.sql +++ b/web/prisma/triggers/07_notifications_service_suggestion.sql @@ -3,7 +3,20 @@ RETURNS TRIGGER AS $$ DECLARE suggestion_status_change "ServiceSuggestionStatusChange"; BEGIN - IF TG_OP = 'INSERT' THEN -- Corresponds to ServiceSuggestionMessage insert + IF TG_OP = 'INSERT' AND TG_TABLE_NAME = 'ServiceSuggestion' THEN -- Corresponds to ServiceSuggestion insert + -- Notify all admins when a new suggestion is created + INSERT INTO "Notification" ("userId", "type", "aboutServiceSuggestionId") + SELECT u."id", 'SUGGESTION_CREATED', NEW."id" + FROM "User" u + WHERE u."admin" = true + AND NOT EXISTS ( + SELECT 1 FROM "Notification" n + WHERE n."userId" = u."id" + AND n."type" = 'SUGGESTION_CREATED' + AND n."aboutServiceSuggestionId" = NEW."id" + ); + + ELSIF TG_OP = 'INSERT' AND TG_TABLE_NAME = 'ServiceSuggestionMessage' THEN -- Corresponds to ServiceSuggestionMessage insert -- Notify suggestion author (if not the sender) INSERT INTO "Notification" ("userId", "type", "aboutServiceSuggestionId", "aboutServiceSuggestionMessageId") SELECT s."userId", 'SUGGESTION_MESSAGE', NEW."suggestionId", NEW."id" @@ -55,6 +68,13 @@ BEGIN END; $$ LANGUAGE plpgsql; +-- Trigger for new suggestions +DROP TRIGGER IF EXISTS service_suggestion_created_notifications_trigger ON "ServiceSuggestion"; +CREATE TRIGGER service_suggestion_created_notifications_trigger + AFTER INSERT ON "ServiceSuggestion" + FOR EACH ROW + EXECUTE FUNCTION trigger_service_suggestion_notifications(); + -- Trigger for new messages DROP TRIGGER IF EXISTS service_suggestion_message_notifications_trigger ON "ServiceSuggestionMessage"; CREATE TRIGGER service_suggestion_message_notifications_trigger diff --git a/web/src/actions/serviceSuggestion.ts b/web/src/actions/serviceSuggestion.ts index a46b424..7566613 100644 --- a/web/src/actions/serviceSuggestion.ts +++ b/web/src/actions/serviceSuggestion.ts @@ -36,6 +36,10 @@ const findPossibleDuplicates = async (input: { name: string }) => { id: { in: matches.map(({ id }) => id), }, + listedAt: { lte: new Date() }, + serviceVisibility: { + in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'], + }, }, select: { id: true, @@ -58,6 +62,8 @@ const serializeExtraNotes = >( serializedValue = value } else if (value === undefined || value === null) { serializedValue = '' + } else if (Array.isArray(value)) { + serializedValue = value.map((item) => String(item)).join(', ') } else if (typeof value === 'object' && 'toString' in value && typeof value.toString === 'function') { // eslint-disable-next-line @typescript-eslint/no-base-to-string serializedValue = value.toString() @@ -144,17 +150,7 @@ export const serviceSuggestionActions = { .max(SUGGESTION_SLUG_MAX_LENGTH) .regex(/^[a-z0-9-]+$/, { message: 'Slug must contain only lowercase letters, numbers, and hyphens', - }) - .refine( - async (slug) => { - const exists = await prisma.service.findUnique({ - select: { id: true }, - where: { slug }, - }) - return !exists - }, - { message: 'Slug must be unique, try a different one' } - ), + }), description: z.string().min(1).max(SUGGESTION_DESCRIPTION_MAX_LENGTH), allServiceUrls: stringListOfUrlsSchemaRequired, tosUrls: stringListOfUrlsSchemaRequired, @@ -189,8 +185,16 @@ export const serviceSuggestionActions = { location: 'serviceSuggestion.createService', }) + const serviceWithSameSlug = await prisma.service.findUnique({ + select: { id: true, name: true, slug: true, description: true }, + where: { slug: input.slug }, + }) + if (!input.skipDuplicateCheck) { - const possibleDuplicates = await findPossibleDuplicates(input) + const possibleDuplicates = [ + ...(serviceWithSameSlug ? [serviceWithSameSlug] : []), + ...(await findPossibleDuplicates(input)), + ] if (possibleDuplicates.length > 0) { return { @@ -208,6 +212,13 @@ export const serviceSuggestionActions = { service: undefined, } as const } + } else { + if (serviceWithSameSlug) { + throw new ActionError({ + message: 'Slug already in use, try a different one', + code: 'BAD_REQUEST', + }) + } } const imageUrl = await saveFileLocally(input.imageFile, input.imageFile.name) diff --git a/web/src/components/BaseHead.astro b/web/src/components/BaseHead.astro index 68c926d..b619d32 100644 --- a/web/src/components/BaseHead.astro +++ b/web/src/components/BaseHead.astro @@ -13,6 +13,7 @@ import HtmxScript from './HtmxScript.astro' import NotificationEventsScript from './NotificationEventsScript.astro' import { makeOgImageUrl } from './OgImage' import ServerEventsScript from './ServerEventsScript.astro' +import ServiceWorkerScript from './ServiceWorkerScript.astro' import TailwindJsPluggin from './TailwindJsPluggin.astro' import type { ComponentProps } from 'astro/types' @@ -137,10 +138,12 @@ const ogImageUrl = makeOgImageUrl(ogImage, Astro.url) )) } - - - - - - - +{ + Astro.locals.user && ( + <> + + + + + ) +} diff --git a/web/src/components/DynamicFavicon.astro b/web/src/components/DynamicFavicon.astro index ce83d40..ffd4376 100644 --- a/web/src/components/DynamicFavicon.astro +++ b/web/src/components/DynamicFavicon.astro @@ -66,7 +66,7 @@ function addBadgeIfUnread(href: string) { } diff --git a/web/src/constants/notificationTypes.ts b/web/src/constants/notificationTypes.ts index fdfb44a..73f9dd3 100644 --- a/web/src/constants/notificationTypes.ts +++ b/web/src/constants/notificationTypes.ts @@ -40,6 +40,11 @@ export const { label: 'New comment/rating', icon: 'ri:chat-4-line', }, + { + id: 'SUGGESTION_CREATED', + label: 'New suggestion', + icon: 'ri:lightbulb-line', + }, { id: 'SUGGESTION_MESSAGE', label: 'New message in suggestion', diff --git a/web/src/constants/serviceSuggestionType.ts b/web/src/constants/serviceSuggestionType.ts index 15e0ee4..974593f 100644 --- a/web/src/constants/serviceSuggestionType.ts +++ b/web/src/constants/serviceSuggestionType.ts @@ -8,6 +8,7 @@ type ServiceSuggestionTypeInfo = { value: T slug: string label: string + labelAlt: string icon: string order: number default: boolean @@ -33,12 +34,14 @@ export const { order: Infinity, default: false, color: 'zinc', + labelAlt: value ? transformCase(value.replace('_', ' '), 'title') : String(value), }), [ { value: 'CREATE_SERVICE', slug: 'create', label: 'Create', + labelAlt: 'service', icon: 'ri:add-line', order: 1, default: true, @@ -48,6 +51,7 @@ export const { value: 'EDIT_SERVICE', slug: 'edit', label: 'Edit', + labelAlt: 'edit', icon: 'ri:pencil-line', order: 2, default: false, diff --git a/web/src/layouts/BaseLayout.astro b/web/src/layouts/BaseLayout.astro index 3f750cf..cc2a87e 100644 --- a/web/src/layouts/BaseLayout.astro +++ b/web/src/layouts/BaseLayout.astro @@ -16,7 +16,7 @@ type Props = ComponentProps & { children: AstroChildren errors?: string[] success?: string[] - className?: { + classNames?: { body?: string main?: string footer?: string @@ -31,14 +31,16 @@ type Props = ComponentProps & { | 'max-w-screen-sm' | 'max-w-screen-xl' | 'max-w-screen-xs' + isErrorPage?: boolean } const { errors = [], success = [], - className, + classNames, widthClassName = 'max-w-screen-2xl', showSplashText, + isErrorPage, ...baseHeadProps } = Astro.props @@ -77,7 +79,10 @@ const announcement = await Astro.locals.banners.try( - + {announcement && }
-