diff --git a/src/app.css b/src/app.css index 36167a1..ae1a4e6 100644 --- a/src/app.css +++ b/src/app.css @@ -443,9 +443,9 @@ main pre code, } /* Zitate: Lora (Design System – nur für Zitate) */ -[data-block-type="quote"] blockquote, -[data-block-type="quote"] blockquote p, -[data-block-type="quote"] blockquote cite { +blockquote[data-block-type="quote"], +blockquote[data-block-type="quote"] p, +blockquote[data-block-type="quote"] cite { font-family: var(--font-secondary); } diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index 26a1750..c7a894d 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -136,7 +136,7 @@ export interface QuoteBlockData { _slug?: string; quote?: string; author?: string; - variant?: "left" | "right"; + variant?: "left" | "right" | "center"; layout?: BlockLayout; } @@ -151,11 +151,12 @@ export interface QuoteCarouselBlockData { _slug?: string; quote?: string; author?: string; - variant?: "left" | "right"; + variant?: "left" | "right" | "center"; } >; autoRotate?: boolean; intervalSeconds?: number; + showArrows?: boolean; layout?: BlockLayout; } diff --git a/src/lib/cms-api.generated.ts b/src/lib/cms-api.generated.ts index 7a21139..d9d806d 100644 --- a/src/lib/cms-api.generated.ts +++ b/src/lib/cms-api.generated.ts @@ -562,6 +562,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/collections/quote_carousel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get schema definition for 'quote_carousel' */ + get: operations["getQuoteCarouselSchema"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/collections/searchable_text": { parameters: { query?: never; @@ -2334,6 +2351,63 @@ export interface paths { patch?: never; trace?: never; }; + "/api/content/quote_carousel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List 'quote_carousel' entries */ + get: operations["listQuoteCarousel"]; + put?: never; + /** Create a new 'quote_carousel' entry */ + post: operations["createQuoteCarousel"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/content/quote_carousel/{slug}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get 'quote_carousel' entry by slug */ + get: operations["getQuoteCarousel"]; + /** Update 'quote_carousel' entry */ + put: operations["updateQuoteCarousel"]; + post?: never; + /** Delete 'quote_carousel' entry */ + delete: operations["deleteQuoteCarousel"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/content/quote_carousel/{slug}/referrers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List referrers of 'quote_carousel' entry + * @description Returns all entries that reference this entry (reverse index). Empty when not using referrer index (e.g. with RUSTYCMS_ENVIRONMENTS). + */ + get: operations["getQuoteCarouselReferrers"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/content/searchable_text": { parameters: { query?: never; @@ -3207,6 +3281,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -3326,6 +3402,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -4383,6 +4461,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -4401,6 +4481,8 @@ export interface components { page_config: { /** @description Entry identifier (filename) */ readonly _slug?: string; + /** @description analytics.pm86.de Site-ID (z. B. ad2858cf2db1). Leer = kein Tracking. */ + analyticsPm86SiteId?: string; /** @description Headline for blog overview page (optional) */ blogPostsPageHeadline?: string; /** @description Subheadline for blog overview page (optional) */ @@ -4425,6 +4507,8 @@ export interface components { page_config_input: { /** @description URL slug (used as filename) */ _slug: string; + /** @description analytics.pm86.de Site-ID (z. B. ad2858cf2db1). Leer = kein Tracking. */ + analyticsPm86SiteId?: string; /** @description Headline for blog overview page (optional) */ blogPostsPageHeadline?: string; /** @description Subheadline for blog overview page (optional) */ @@ -4499,6 +4583,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -4604,6 +4690,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -4710,6 +4798,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -4823,6 +4913,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -4929,6 +5021,8 @@ export interface components { row3JustifyContent: "start" | "end" | "center" | "between" | "around" | "evenly"; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -5039,7 +5133,103 @@ export interface components { * @default left * @enum {string} */ - variant: "left" | "right"; + variant: "left" | "right" | "center"; + }; + quote_carousel: { + /** @description Entry identifier (filename) */ + readonly _slug?: string; + /** + * @description Auto-advance to next quote + * @default true + */ + autoRotate: boolean; + /** @description Optional block headline */ + headline?: string; + /** + * @description Seconds between auto-advances (if autoRotate) + * @default 6 + */ + intervalSeconds: number; + /** @description Column width (grid). Optional. */ + layout?: { + /** + * @description Breakout layout (full width) + * @default false + */ + breakout: boolean; + /** + * @description Width on desktop (optional) + * @enum {string} + */ + desktop?: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; + /** + * @description Width on mobile (1–12) + * @default 12 + * @enum {string} + */ + mobile: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; + /** + * @description Space below (rem) + * @default 0 + * @enum {number} + */ + spaceBottom: 0 | 0.5 | 1 | 1.5 | 2; + /** + * @description Width on tablet (optional) + * @enum {string} + */ + tablet?: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; + }; + /** @description Referenced quote entries to rotate through */ + quotes: string[]; + }; + quote_carousel_input: { + /** @description URL slug (used as filename) */ + _slug: string; + /** + * @description Auto-advance to next quote + * @default true + */ + autoRotate: boolean; + /** @description Optional block headline */ + headline?: string; + /** + * @description Seconds between auto-advances (if autoRotate) + * @default 6 + */ + intervalSeconds: number; + /** @description Column width (grid). Optional. */ + layout?: { + /** + * @description Breakout layout (full width) + * @default false + */ + breakout: boolean; + /** + * @description Width on desktop (optional) + * @enum {string} + */ + desktop?: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; + /** + * @description Width on mobile (1–12) + * @default 12 + * @enum {string} + */ + mobile: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; + /** + * @description Space below (rem) + * @default 0 + * @enum {number} + */ + spaceBottom: 0 | 0.5 | 1 | 1.5 | 2; + /** + * @description Width on tablet (optional) + * @enum {string} + */ + tablet?: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; + }; + /** @description Referenced quote entries to rotate through */ + quotes: string[]; }; quote_input: { /** @description URL slug (used as filename) */ @@ -5083,7 +5273,7 @@ export interface components { * @default left * @enum {string} */ - variant: "left" | "right"; + variant: "left" | "right" | "center"; }; searchable_text: { /** @description Entry identifier (filename) */ @@ -5178,6 +5368,8 @@ export interface components { readonly _slug?: string; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -5192,6 +5384,8 @@ export interface components { _slug: string; /** @description Meta description for search engines (optional) */ seoDescription?: string; + /** @description Comma-separated keywords (meta name=keywords). Google ignoriert, Bing/Yandex partial; optional. */ + seoKeywords?: string; /** * @description Robots meta directive * @default index, follow @@ -6272,6 +6466,31 @@ export interface operations { }; }; }; + getQuoteCarouselSchema: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Schema definition */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Collection not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getSearchableTextSchema: { parameters: { query?: never; @@ -12090,6 +12309,8 @@ export interface operations { seoTitle?: string; /** @description Filter by seoDescription */ seoDescription?: string; + /** @description Filter by seoKeywords */ + seoKeywords?: string; /** @description Filter by seoMetaRobots */ seoMetaRobots?: string; /** @description Filter by slug */ @@ -12358,6 +12579,8 @@ export interface operations { siteName?: string; /** @description Filter by website */ website?: string; + /** @description Filter by analyticsPm86SiteId */ + analyticsPm86SiteId?: string; }; header?: never; path?: never; @@ -12612,6 +12835,8 @@ export interface operations { seoTitle?: string; /** @description Filter by seoDescription */ seoDescription?: string; + /** @description Filter by seoKeywords */ + seoKeywords?: string; /** @description Filter by seoMetaRobots */ seoMetaRobots?: string; /** @description Filter by content */ @@ -12900,6 +13125,8 @@ export interface operations { seoTitle?: string; /** @description Filter by seoDescription */ seoDescription?: string; + /** @description Filter by seoKeywords */ + seoKeywords?: string; /** @description Filter by seoMetaRobots */ seoMetaRobots?: string; /** @description Filter by id */ @@ -13623,6 +13850,248 @@ export interface operations { }; }; }; + listQuoteCarousel: { + parameters: { + query?: { + /** @description Field name to sort by */ + _sort?: string; + /** @description Sort order (default: asc) */ + _order?: "asc" | "desc"; + /** @description Page number (1-indexed). Ignored if _limit/_offset is set. */ + _page?: number; + /** @description Items per page (max 1000). Ignored if _limit/_offset is set. */ + _per_page?: number; + /** @description Maximum number of items to return (max 1000). Takes precedence over _per_page. */ + _limit?: number; + /** @description Number of items to skip. Takes precedence over _page. */ + _offset?: number; + /** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */ + _resolve?: string; + /** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */ + _locale?: string; + /** @description Filter by headline */ + headline?: string; + /** @description Filter by quotes */ + quotes?: string; + /** @description Filter by autoRotate */ + autoRotate?: boolean; + /** @description Filter by intervalSeconds */ + intervalSeconds?: number; + /** @description Filter by layout */ + layout?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of entries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + items?: components["schemas"]["quote_carousel"][]; + page?: number; + per_page?: number; + total?: number; + total_pages?: number; + }; + }; + }; + }; + }; + createQuoteCarousel: { + parameters: { + query?: { + /** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */ + _resolve?: string; + /** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */ + _locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["quote_carousel_input"]; + }; + }; + responses: { + /** @description Entry created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["quote_carousel"]; + }; + }; + /** @description Validation error */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Entry already exists */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getQuoteCarousel: { + parameters: { + query?: { + /** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */ + _resolve?: string; + /** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */ + _locale?: string; + }; + header?: never; + path: { + /** @description Entry identifier (filename without extension) */ + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["quote_carousel"]; + }; + }; + /** @description Entry not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + updateQuoteCarousel: { + parameters: { + query?: { + /** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */ + _resolve?: string; + /** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */ + _locale?: string; + }; + header?: never; + path: { + /** @description Entry identifier (filename without extension) */ + slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["quote_carousel"]; + }; + }; + responses: { + /** @description Entry updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["quote_carousel"]; + }; + }; + /** @description Validation error */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Entry not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteQuoteCarousel: { + parameters: { + query?: { + /** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */ + _resolve?: string; + /** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */ + _locale?: string; + }; + header?: never; + path: { + /** @description Entry identifier (filename without extension) */ + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Entry deleted */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Entry not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getQuoteCarouselReferrers: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Entry slug */ + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of referrers (collection, slug, field, locale) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Collection of the referring entry */ + collection?: string; + /** @description Field that holds the reference */ + field?: string; + /** @description Locale of the referring entry if applicable */ + locale?: string | null; + /** @description Slug of the referring entry */ + slug?: string; + }[]; + }; + }; + }; + }; listSearchableText: { parameters: { query?: { @@ -13890,6 +14359,8 @@ export interface operations { seoTitle?: string; /** @description Filter by seoDescription */ seoDescription?: string; + /** @description Filter by seoKeywords */ + seoKeywords?: string; /** @description Filter by seoMetaRobots */ seoMetaRobots?: string; }; diff --git a/src/lib/components/blocks/QuoteBlock.svelte b/src/lib/components/blocks/QuoteBlock.svelte index 990ead7..8ea2326 100644 --- a/src/lib/components/blocks/QuoteBlock.svelte +++ b/src/lib/components/blocks/QuoteBlock.svelte @@ -2,17 +2,36 @@ import { getBlockLayoutClasses } from "$lib/block-layout"; import type { QuoteBlockData } from "$lib/block-types"; - let { block }: { block: QuoteBlockData } = $props(); + let { + block, + bare = false, + extraClass = "", + }: { block: QuoteBlockData; bare?: boolean; extraClass?: string } = $props(); const layoutClasses = $derived(getBlockLayoutClasses(block.layout)); - const variantClass = $derived(block.variant === "right" ? "text-right" : "text-left"); + const isCenter = $derived(block.variant === "center"); + const alignClass = $derived( + isCenter ? "text-center" : block.variant === "right" ? "text-right" : "text-left", + ); + const borderClass = $derived(isCenter ? "" : "border-l-4 border-green-700 pl-4"); -
-
-

"{block.quote ?? ""}"

+{#snippet quoteMarkup()} +
+

"{block.quote ?? ""}"

{#if block.author} - — {block.author} + {block.author} {/if}
-
+{/snippet} + +{#if bare} + {@render quoteMarkup()} +{:else} +
+ {@render quoteMarkup()} +
+{/if} diff --git a/src/lib/components/blocks/QuoteCarouselBlock.svelte b/src/lib/components/blocks/QuoteCarouselBlock.svelte index 174b98f..93d19ba 100644 --- a/src/lib/components/blocks/QuoteCarouselBlock.svelte +++ b/src/lib/components/blocks/QuoteCarouselBlock.svelte @@ -1,12 +1,12 @@