diff --git a/.changeset/beige-comics-serve.md b/.changeset/beige-comics-serve.md new file mode 100644 index 00000000000..c5e55cb225e --- /dev/null +++ b/.changeset/beige-comics-serve.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": patch +--- + +Optimize responsive behavior of `CrudMoreActionsMenu` diff --git a/.changeset/gentle-boats-hunt.md b/.changeset/gentle-boats-hunt.md new file mode 100644 index 00000000000..67f037bb34e --- /dev/null +++ b/.changeset/gentle-boats-hunt.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-admin": minor +--- + +Adapt styling of `DamTable` to align with Comet DXP design diff --git a/.changeset/hip-eagles-play.md b/.changeset/hip-eagles-play.md deleted file mode 100644 index 1d651143b0c..00000000000 --- a/.changeset/hip-eagles-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@comet/cms-site": patch ---- - -Fix `PixelImageBlock` issue when setting fixed height and width auto diff --git a/.env b/.env index 090c739d3c7..7c6996d5e85 100644 --- a/.env +++ b/.env @@ -1,6 +1,8 @@ # node NODE_ENV=development +SERVER_HOST=localhost # change me to 0.0.0.0 if you want to access the server from other devices + # project PROJECT_NAME=comet-demo DEV_DOMAIN_POSTFIX=demo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d74ecb20e8..9bdb7dec645 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,7 +83,7 @@ Example for a good changeset: ````md --- -"@comet/cms-site": minor +"@comet/site-nextjs": minor --- Add `ErrorHandlerProvider` @@ -110,7 +110,7 @@ The `ErrorHandler` receives the errors in the application and can report them to ```tsx "use client"; -import { ErrorHandlerProvider } from "@comet/cms-site"; +import { ErrorHandlerProvider } from "@comet/site-nextjs"; import { PropsWithChildren } from "react"; export function ErrorHandler({ children }: PropsWithChildren) { diff --git a/copy-schema-files.js b/copy-schema-files.js index cca4fa93a4b..2d449355de2 100644 --- a/copy-schema-files.js +++ b/copy-schema-files.js @@ -5,6 +5,8 @@ const fs = require("fs"); fs.promises.copyFile("packages/api/cms-api/schema.gql", "packages/admin/cms-admin/schema.gql"), fs.promises.copyFile("packages/api/cms-api/block-meta.json", "packages/admin/cms-admin/block-meta.json"), fs.promises.copyFile("packages/api/cms-api/block-meta.json", "packages/site/cms-site/block-meta.json"), + fs.promises.copyFile("packages/api/cms-api/block-meta.json", "packages/site/site-nextjs/block-meta.json"), + fs.promises.copyFile("packages/api/cms-api/block-meta.json", "packages/site/site-react/block-meta.json"), fs.promises.copyFile("demo/api/block-meta.json", "demo/admin/block-meta.json"), fs.promises.copyFile("demo/api/block-meta.json", "demo/site/block-meta.json"), diff --git a/demo/admin/package.json b/demo/admin/package.json index bcc7f361bd2..01133501b35 100644 --- a/demo/admin/package.json +++ b/demo/admin/package.json @@ -19,7 +19,8 @@ "lint:tsc": "tsc --project .", "preview": "npm run build && vite preview", "serve": "node server", - "start": "run-s admin-generator intl:compile && run-p gql:types generate-block-types && dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- vite" + "start": "$npm_execpath check-node-version && run-s admin-generator intl:compile && run-p gql:types generate-block-types && dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- vite", + "check-node-version": "check-node-version --node $(cat ../../.nvmrc)" }, "dependencies": { "@apollo/client": "^3.13.4", @@ -76,6 +77,7 @@ "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react-swc": "^3.8.1", + "check-node-version": "^4.2.1", "chokidar-cli": "^3.0.0", "cosmiconfig-toml-loader": "^1.0.0", "dotenv-cli": "^7.4.4", diff --git a/demo/admin/server/index.js b/demo/admin/server/index.js index 9f8d1808e0c..1bfcce62dd4 100644 --- a/demo/admin/server/index.js +++ b/demo/admin/server/index.js @@ -7,6 +7,7 @@ const { createProxyMiddleware } = require("http-proxy-middleware"); const app = express(); const port = process.env.APP_PORT ?? 3000; +const host = process.env.SERVER_HOST ?? "localhost"; let indexFile = fs.readFileSync("./build/index.html", "utf8"); @@ -38,6 +39,7 @@ app.use( ); app.get("/status/health", (req, res) => { + res.setHeader("cache-control", "no-store"); res.send("OK!"); }); @@ -71,6 +73,6 @@ app.get("*", (req, res) => { res.send(indexFile); }); -app.listen(port, () => { - console.log(`Admin app listening at http://localhost:${port}`); +app.listen(port, host, () => { + console.log(`Admin app listening at http://${host}:${port}`); }); diff --git a/demo/admin/vite.config.mts b/demo/admin/vite.config.mts index 27718704d5f..04c973e2c4d 100644 --- a/demo/admin/vite.config.mts +++ b/demo/admin/vite.config.mts @@ -75,7 +75,7 @@ export default defineConfig(({ mode }) => { adminPackagesHotReloadPlugin, ], server: { - host: true, + host: process.env.SERVER_HOST ?? "localhost", port: Number(process.env.ADMIN_PORT), proxy: process.env.API_URL_INTERNAL ? { diff --git a/demo/api/package.json b/demo/api/package.json index c3063aba534..0b2bdf447ca 100644 --- a/demo/api/package.json +++ b/demo/api/package.json @@ -21,7 +21,8 @@ "lint:tsc": "tsc --project ./tsconfig.lint.json", "mikro-orm": "dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- mikro-orm", "start": "$npm_execpath prebuild && dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- nest start --debug --watch --preserveWatchOutput", - "start:dev": "run-p dev:*", + "check-node-version": "check-node-version --node $(cat ../../.nvmrc)", + "start:dev": "$npm_execpath check-node-version && run-p dev:*", "start:prod": "node dist/main" }, "dependencies": { @@ -75,6 +76,7 @@ "@types/inquirer": "^8.2.11", "@types/node": "^22.15.21", "@types/response-time": "^2.3.8", + "check-node-version": "^4.2.1", "chokidar-cli": "^3.0.0", "dotenv-cli": "^7.4.4", "eslint": "^9.22.0", diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 6dc13fba8c7..b95c6570729 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -459,7 +459,7 @@ type PageTreeNode { category: PageTreeNodeCategory! userGroup: UserGroup! childNodes: [PageTreeNode!]! - numberOfDescendants: Float! + numberOfDescendants: Int! parentNode: PageTreeNode path: String! parentNodes: [PageTreeNode!]! @@ -647,6 +647,7 @@ type Redirect { target: JSONObject! comment: String active: Boolean! + activatedAt: DateTime generationType: RedirectGenerationType! createdAt: DateTime! updatedAt: DateTime! diff --git a/demo/api/src/app.module.ts b/demo/api/src/app.module.ts index ea488513b5d..447120f0eb9 100644 --- a/demo/api/src/app.module.ts +++ b/demo/api/src/app.module.ts @@ -53,6 +53,7 @@ import { PageTreeNode } from "./page-tree/entities/page-tree-node.entity"; import { ProductsModule } from "./products/products.module"; import { RedirectScope } from "./redirects/dto/redirect-scope"; import { RedirectTargetUrlService } from "./redirects/redirect-target-url.service"; +import { StatusModule } from "./status/status.module"; @Module({}) export class AppModule { @@ -152,6 +153,7 @@ export class AppModule { File: DamFile, Folder: DamFolder, }), + StatusModule, FileUploadsModule.register({ maxFileSize: config.fileUploads.maxFileSize, directory: `${config.blob.storageDirectoryPrefix}-file-uploads`, diff --git a/demo/api/src/comet-config.json b/demo/api/src/comet-config.json index adeb437805d..5b30d9debea 100644 --- a/demo/api/src/comet-config.json +++ b/demo/api/src/comet-config.json @@ -8,7 +8,7 @@ "maxFileSize": 15 }, "images": { - "deviceSizes": [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + "deviceSizes": [640, 750, 828, 1080, 1200, 1920, 2048, 2560, 3200, 3840], "imageSizes": [16, 32, 48, 64, 96, 128, 256, 320, 384] }, "imgproxy": { diff --git a/demo/api/src/config/config.ts b/demo/api/src/config/config.ts index bdf5bd7500c..07479125c89 100644 --- a/demo/api/src/config/config.ts +++ b/demo/api/src/config/config.ts @@ -27,6 +27,7 @@ export function createConfig(processEnv: NodeJS.ProcessEnv) { return { ...cometConfig, debug: processEnv.NODE_ENV !== "production", + serverHost: processEnv.SERVER_HOST ?? "localhost", helmRelease: envVars.HELM_RELEASE, apiUrl: envVars.API_URL, apiPort: envVars.API_PORT, diff --git a/demo/api/src/main.ts b/demo/api/src/main.ts index 0d2daba961c..46dae6c2d7a 100644 --- a/demo/api/src/main.ts +++ b/demo/api/src/main.ts @@ -71,7 +71,8 @@ async function bootstrap(): Promise { } const port = config.apiPort; - await app.listen(port); - console.log(`Application is running on: http://localhost:${port}/`); + const host = config.serverHost; + await app.listen(port, host); + console.log(`Application is running on: http://${host}:${port}/`); } bootstrap(); diff --git a/demo/api/src/products/entities/manufacturer.entity.ts b/demo/api/src/products/entities/manufacturer.entity.ts index 97a8e7102f1..535b071ea34 100644 --- a/demo/api/src/products/entities/manufacturer.entity.ts +++ b/demo/api/src/products/entities/manufacturer.entity.ts @@ -14,7 +14,7 @@ export class AlternativeAddress { @IsString() street: string; - @Field(() => Number, { nullable: true }) + @Field({ nullable: true }) @Property({ nullable: true }) @IsNumber() @IsNullable() @@ -51,7 +51,7 @@ export class AlternativeAddressAsEmbeddable { @IsString() street: string; - @Field(() => Number, { nullable: true }) + @Field({ nullable: true }) @Property({ nullable: true }) @IsNumber() @IsNullable() diff --git a/demo/api/src/status/status.controller.ts b/demo/api/src/status/status.controller.ts new file mode 100644 index 00000000000..2e0487bb12c --- /dev/null +++ b/demo/api/src/status/status.controller.ts @@ -0,0 +1,31 @@ +import { DisableCometGuards } from "@comet/cms-api"; +import { EntityManager } from "@mikro-orm/postgresql"; +import { Controller, Get, Header } from "@nestjs/common"; + +@Controller("status") +@DisableCometGuards() +export class StatusController { + constructor(private readonly entityManager: EntityManager) {} + + @Get("liveness") + @Header("cache-control", "no-store") + liveness(): string { + // If this controller returns a non 2xx status code, the pod is restarted by kubernetes + // see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + return "OK"; + } + + @Get("readiness") + @Header("cache-control", "no-store") + async readiness(): Promise { + // If this controller returns a non 2xx status code, the pod does not receive traffic + // If the database is not available, it does not make sense to restart the pod + // However, the pod should not receive traffic anymore as it can't handle it without the database + // see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes + // + // If your application is not relying on the database, you can remove the next line + // If your application is relying on another service (e.g. redis), add a health-check here + await this.entityManager.execute("SELECT 1+1"); + return "OK"; + } +} diff --git a/demo/api/src/status/status.module.ts b/demo/api/src/status/status.module.ts new file mode 100644 index 00000000000..62bea9872bf --- /dev/null +++ b/demo/api/src/status/status.module.ts @@ -0,0 +1,6 @@ +import { Module } from "@nestjs/common"; + +import { StatusController } from "./status.controller"; + +@Module({ controllers: [StatusController] }) +export class StatusModule {} diff --git a/demo/build-and-run-site.sh b/demo/build-and-run-site.sh index c0465694f1e..a38d8a956a2 100755 --- a/demo/build-and-run-site.sh +++ b/demo/build-and-run-site.sh @@ -1,5 +1,3 @@ -# Execute this script via `npm run build-and-run-site` -# # This script builds the site like in the CI and starts it. # # Reasons why you want to do this: diff --git a/demo/site-pages/src/common/blocks/CallToActionListBlock.tsx b/demo/site-pages/src/common/blocks/CallToActionListBlock.tsx index 8c3e9399ac5..77110305275 100644 --- a/demo/site-pages/src/common/blocks/CallToActionListBlock.tsx +++ b/demo/site-pages/src/common/blocks/CallToActionListBlock.tsx @@ -21,7 +21,7 @@ const Root = styled.div` flex-flow: row wrap; gap: ${({ theme }) => theme.spacing.S300}; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { gap: ${({ theme }) => theme.spacing.S400}; } `; diff --git a/demo/site-pages/src/common/blocks/MediaGalleryBlock.tsx b/demo/site-pages/src/common/blocks/MediaGalleryBlock.tsx index 81048396364..c9f156329f9 100644 --- a/demo/site-pages/src/common/blocks/MediaGalleryBlock.tsx +++ b/demo/site-pages/src/common/blocks/MediaGalleryBlock.tsx @@ -53,15 +53,15 @@ const PageLayoutContent = styled.div` grid-column: 2 / -2; position: relative; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { grid-column: 5 / -5; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { grid-column: 6 / -6; } - ${({ theme }) => theme.breakpoints.lg.mediaQuery} { + ${({ theme }) => theme.breakpoints.xl.mediaQuery} { grid-column: 7 / -7; } `; diff --git a/demo/site-pages/src/common/blocks/RichTextBlock.tsx b/demo/site-pages/src/common/blocks/RichTextBlock.tsx index 698fe9a128c..029644047b1 100644 --- a/demo/site-pages/src/common/blocks/RichTextBlock.tsx +++ b/demo/site-pages/src/common/blocks/RichTextBlock.tsx @@ -116,7 +116,7 @@ const DisableLastBottomSpacing = styled.div` > *:last-child { margin-bottom: 0; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { margin-bottom: 0; } } diff --git a/demo/site-pages/src/common/components/Typography.tsx b/demo/site-pages/src/common/components/Typography.tsx index 12dce9ed8c9..af59504a1af 100644 --- a/demo/site-pages/src/common/components/Typography.tsx +++ b/demo/site-pages/src/common/components/Typography.tsx @@ -205,7 +205,7 @@ export const Typography = styled.div.attrs<{ css` margin-bottom: 0; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { margin-bottom: 0; } `}; diff --git a/demo/site-pages/src/common/helpers/SvgUse.tsx b/demo/site-pages/src/common/helpers/SvgUse.tsx index a8200c23a5c..2ec0160572d 100644 --- a/demo/site-pages/src/common/helpers/SvgUse.tsx +++ b/demo/site-pages/src/common/helpers/SvgUse.tsx @@ -5,7 +5,7 @@ interface SvgUseProps extends SVGProps { } export const SvgUse = ({ href, ...props }: SvgUseProps) => ( - + ); diff --git a/demo/site-pages/src/documents/pages/blocks/BasicStageBlock.tsx b/demo/site-pages/src/documents/pages/blocks/BasicStageBlock.tsx index 55c940981f7..66fd1b1b595 100644 --- a/demo/site-pages/src/documents/pages/blocks/BasicStageBlock.tsx +++ b/demo/site-pages/src/documents/pages/blocks/BasicStageBlock.tsx @@ -72,7 +72,7 @@ const Content = styled.div<{ $alignItems: CSSProperties["alignItems"] }>` const MediaPhone = styled.div` height: 800px; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: none; } `; @@ -81,11 +81,11 @@ const MediaTablet = styled.div` display: none; height: 700px; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: none; } `; @@ -94,11 +94,11 @@ const MediaTabletLandscape = styled.div` display: none; height: 650px; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: none; } `; @@ -107,11 +107,11 @@ const MediaDesktop = styled.div` display: none; height: 750px; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.lg.mediaQuery} { + ${({ theme }) => theme.breakpoints.xl.mediaQuery} { height: 800px; } `; diff --git a/demo/site-pages/src/documents/pages/blocks/BillboardTeaserBlock.tsx b/demo/site-pages/src/documents/pages/blocks/BillboardTeaserBlock.tsx index 021ef4e2aeb..2d001e5e8b8 100644 --- a/demo/site-pages/src/documents/pages/blocks/BillboardTeaserBlock.tsx +++ b/demo/site-pages/src/documents/pages/blocks/BillboardTeaserBlock.tsx @@ -69,18 +69,18 @@ const Content = styled.div` ${({ theme }) => css` grid-column: 3 / -3; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { grid-column: 5 / -5; } - ${theme.breakpoints.lg.mediaQuery} { + ${theme.breakpoints.xl.mediaQuery} { grid-column: 7 / -7; } `}; `; const ImageMobile = styled.div` - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: none; } `; @@ -88,11 +88,11 @@ const ImageMobile = styled.div` const ImageTablet = styled.div` display: none; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: none; } `; @@ -100,11 +100,11 @@ const ImageTablet = styled.div` const ImageDesktop = styled.div` display: none; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: none; } `; @@ -112,7 +112,7 @@ const ImageDesktop = styled.div` const ImageLargeDesktop = styled.div` display: none; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: block; } `; diff --git a/demo/site-pages/src/documents/pages/blocks/ColumnsBlock.tsx b/demo/site-pages/src/documents/pages/blocks/ColumnsBlock.tsx index 625c4336d58..312c1e245ad 100644 --- a/demo/site-pages/src/documents/pages/blocks/ColumnsBlock.tsx +++ b/demo/site-pages/src/documents/pages/blocks/ColumnsBlock.tsx @@ -50,21 +50,21 @@ const Column = styled.div<{ $layout: string }>` css` grid-column: 5 / -5; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { grid-column: 7 / -7; } - ${theme.breakpoints.sm.mediaQuery} { + ${theme.breakpoints.md.mediaQuery} { grid-column: 8 / -8; } - ${theme.breakpoints.md.mediaQuery} { + ${theme.breakpoints.lg.mediaQuery} { grid-column: 9 / -9; } - ${theme.breakpoints.lg.mediaQuery} { + ${theme.breakpoints.xl.mediaQuery} { grid-column: 10 / -10; } `}; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { ${({ $layout }) => $layout === "4-16-4" && css` diff --git a/demo/site-pages/src/documents/pages/blocks/KeyFactsBlock.tsx b/demo/site-pages/src/documents/pages/blocks/KeyFactsBlock.tsx index 62b5d432d44..3baefe2e5b5 100644 --- a/demo/site-pages/src/documents/pages/blocks/KeyFactsBlock.tsx +++ b/demo/site-pages/src/documents/pages/blocks/KeyFactsBlock.tsx @@ -31,7 +31,7 @@ const ItemWrapper = styled.div<{ $listItemCount: number }>` css` grid-template-columns: repeat(${Math.min($listItemCount, 2)}, 1fr); - ${theme.breakpoints.sm.mediaQuery} { + ${theme.breakpoints.md.mediaQuery} { grid-template-columns: repeat(${Math.min($listItemCount, 4)}, 1fr); } `} diff --git a/demo/site-pages/src/documents/pages/blocks/TeaserBlock.tsx b/demo/site-pages/src/documents/pages/blocks/TeaserBlock.tsx index ddc3a801fa4..3347076897e 100644 --- a/demo/site-pages/src/documents/pages/blocks/TeaserBlock.tsx +++ b/demo/site-pages/src/documents/pages/blocks/TeaserBlock.tsx @@ -27,7 +27,7 @@ const ItemWrapper = styled.div` gap: ${({ theme }) => theme.spacing.D100}; ${({ theme }) => css` - ${theme.breakpoints.sm.mediaQuery} { + ${theme.breakpoints.md.mediaQuery} { grid-template-columns: repeat(4, 1fr); } `} diff --git a/demo/site-pages/src/documents/pages/blocks/TeaserItemBlock.tsx b/demo/site-pages/src/documents/pages/blocks/TeaserItemBlock.tsx index 231bc1ca55b..40ddd590566 100644 --- a/demo/site-pages/src/documents/pages/blocks/TeaserItemBlock.tsx +++ b/demo/site-pages/src/documents/pages/blocks/TeaserItemBlock.tsx @@ -14,39 +14,38 @@ const descriptionRenderers: Renderers = { export const TeaserItemBlock = withPreview( ({ data: { media, title, description, link } }: PropsWithData) => ( - - - - - - - - - - {title} - - - - - - {link.text} - - - - + + + + + + + + + {title} + + + + + + {link.text} + + + ), { label: "Teaser Item" }, ); -const ItemContent = styled.a` +const Link = styled(LinkBlock)` text-decoration: none; cursor: pointer; display: flex; flex: 1; flex-direction: row; gap: ${({ theme }) => theme.spacing.S300}; + color: ${({ theme }) => theme.palette.text.primary}; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { flex: unset; gap: ${({ theme }) => theme.spacing.S400}; flex-direction: column; @@ -56,7 +55,7 @@ const ItemContent = styled.a` const MediaMobile = styled.div` flex: 1; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: none; } `; @@ -65,7 +64,7 @@ const MediaDesktop = styled.div` flex: 1; display: none; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: block; } `; diff --git a/demo/site-pages/src/theme.ts b/demo/site-pages/src/theme.ts index 34774e70f2e..a849b3b4844 100644 --- a/demo/site-pages/src/theme.ts +++ b/demo/site-pages/src/theme.ts @@ -57,10 +57,10 @@ export const theme = { }, fontFamily: "Arial, sans-serif", breakpoints: { - xs: createBreakpoint(600), - sm: createBreakpoint(900), - md: createBreakpoint(1200), - lg: createBreakpoint(1600), + sm: createBreakpoint(600), + md: createBreakpoint(900), + lg: createBreakpoint(1200), + xl: createBreakpoint(1600), }, spacing: { D100: "var(--spacing-d100)", diff --git a/demo/site/next.config.mjs b/demo/site/next.config.mjs index 8be72eecb6a..5634999d92e 100644 --- a/demo/site/next.config.mjs +++ b/demo/site/next.config.mjs @@ -23,7 +23,7 @@ const nextConfig = { styledComponents: true, }, experimental: { - optimizePackageImports: ["@comet/cms-site"], + optimizePackageImports: ["@comet/site-nextjs"], }, cacheHandler: process.env.REDIS_ENABLED === "true" ? import.meta.resolve("./dist/cache-handler.js").replace("file://", "") : undefined, cacheMaxMemorySize: process.env.REDIS_ENABLED === "true" ? 0 : undefined, // disable default in-memory caching diff --git a/demo/site/package.json b/demo/site/package.json index 35cf7755438..10cb67c857d 100644 --- a/demo/site/package.json +++ b/demo/site/package.json @@ -5,7 +5,8 @@ "scripts": { "build": "run-s intl:compile && run-p gql:types generate-block-types build-server && next build", "build-server": "tsc --project tsconfig.server.json", - "dev": "run-s intl:compile && run-p gql:types generate-block-types build-server && NODE_OPTIONS='--inspect=localhost:9230 --max-old-space-size=512' dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- node dist/server.js", + "check-node-version": "check-node-version --node $(cat ../../.nvmrc)", + "dev": "$npm_execpath check-node-version && run-s intl:compile && run-p gql:types generate-block-types build-server && NODE_OPTIONS='--inspect=localhost:9230 --max-old-space-size=512' dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- node dist/server.js", "export": "next export", "generate-block-types": "comet generate-block-types", "generate-block-types:watch": "chokidar -s \"block-meta.json\" -c \"$npm_execpath generate-block-types\"", @@ -20,7 +21,7 @@ "serve": "NODE_ENV=production node dist/server.js" }, "dependencies": { - "@comet/cms-site": "workspace:*", + "@comet/site-nextjs": "workspace:*", "@formatjs/cli": "^6.6.3", "@next/bundle-analyzer": "^14.2.26", "@opentelemetry/api": "^1.9.0", @@ -57,6 +58,7 @@ "@types/node": "^22.15.21", "@types/react": "^18.3.22", "@types/react-dom": "^18.3.7", + "check-node-version": "^4.2.1", "chokidar-cli": "^3.0.0", "eslint": "^9.22.0", "npm-run-all2": "^5.0.2", diff --git a/demo/site/server.ts b/demo/site/server.ts index 5187c82644a..50578df442a 100644 --- a/demo/site/server.ts +++ b/demo/site/server.ts @@ -5,12 +5,12 @@ import { parse } from "url"; import { withMetrics } from "./opentelemetry-metrics"; const dev = process.env.NODE_ENV !== "production"; -const hostname = "localhost"; +const host = process.env.SERVER_HOST ?? "localhost"; const port = parseInt(process.env.PORT || "3000", 10); const cdnOriginCheckSecret = process.env.CDN_ORIGIN_CHECK_SECRET; // when using middleware `hostname` and `port` must be provided below -const app = next({ dev, hostname, port }); +const app = next({ dev, hostname: host, port }); app.prepare().then(() => { if (process.env.TRACING == "production") { @@ -90,8 +90,8 @@ app.prepare().then(() => { console.error(err); process.exit(1); }) - .listen(port, () => { + .listen(port, host, () => { // eslint-disable-next-line no-console - console.log(`> Ready on http://localhost:${port}`); + console.log(`> Ready on http://${host}:${port}`); }); }); diff --git a/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/layout.tsx b/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/layout.tsx index 6e8a085865d..cf98c67f154 100644 --- a/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/layout.tsx +++ b/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/layout.tsx @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { Footer } from "@src/layout/footer/Footer"; import { footerFragment } from "@src/layout/footer/Footer.fragment"; import { createGraphQLFetch } from "@src/util/graphQLClient"; diff --git a/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/page.tsx b/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/page.tsx index 47dbc9c4732..78aaf0104c4 100644 --- a/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/page.tsx +++ b/demo/site/src/app/[visibility]/[domain]/[language]/[[...path]]/page.tsx @@ -1,6 +1,6 @@ export const dynamic = "error"; -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { type ExternalLinkBlockData, type InternalLinkBlockData, type NewsLinkBlockData, type RedirectsLinkBlockData } from "@src/blocks.generated"; import { documentTypes } from "@src/documents"; import { type GQLPageTreeNodeScope } from "@src/graphql.generated"; diff --git a/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/fragment.ts b/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/fragment.ts index fd9cfe595f7..10d22c4b22a 100644 --- a/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/fragment.ts +++ b/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/fragment.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; export const fragment = gql` fragment NewsDetailPage on News { diff --git a/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/page.tsx b/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/page.tsx index ce6d70be824..8574a4ddde9 100644 --- a/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/page.tsx +++ b/demo/site/src/app/[visibility]/[domain]/[language]/news/[slug]/page.tsx @@ -1,6 +1,6 @@ export const dynamic = "error"; -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { type GQLNewsContentScopeInput } from "@src/graphql.generated"; import { type VisibilityParam } from "@src/middleware/domainRewrite"; import { createGraphQLFetch } from "@src/util/graphQLClient"; diff --git a/demo/site/src/app/[visibility]/[domain]/layout.tsx b/demo/site/src/app/[visibility]/[domain]/layout.tsx index 7e01e274fc1..353d123ccd6 100644 --- a/demo/site/src/app/[visibility]/[domain]/layout.tsx +++ b/demo/site/src/app/[visibility]/[domain]/layout.tsx @@ -1,4 +1,4 @@ -import { SitePreviewProvider } from "@comet/cms-site"; +import { SitePreviewProvider } from "@comet/site-nextjs"; import { getSiteConfigForDomain } from "@src/util/siteConfig"; import { SiteConfigProvider } from "@src/util/SiteConfigProvider"; import { draftMode } from "next/headers"; diff --git a/demo/site/src/app/[visibility]/[domain]/sitemap.ts b/demo/site/src/app/[visibility]/[domain]/sitemap.ts index 72744550baf..2c55c91781b 100644 --- a/demo/site/src/app/[visibility]/[domain]/sitemap.ts +++ b/demo/site/src/app/[visibility]/[domain]/sitemap.ts @@ -2,7 +2,7 @@ import { createSitePath } from "@src/util/createSitePath"; export const dynamic = "force-dynamic"; // don't generate at build time -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { createGraphQLFetch } from "@src/util/graphQLClient"; import { getSiteConfig } from "@src/util/siteConfig"; import { type MetadataRoute } from "next"; diff --git a/demo/site/src/app/block-preview/[domain]/[language]/footer/page.tsx b/demo/site/src/app/block-preview/[domain]/[language]/footer/page.tsx index 6ba9e21dad2..a07175ac9e5 100644 --- a/demo/site/src/app/block-preview/[domain]/[language]/footer/page.tsx +++ b/demo/site/src/app/block-preview/[domain]/[language]/footer/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useBlockPreviewFetch, useIFrameBridge } from "@comet/cms-site"; +import { useBlockPreviewFetch, useIFrameBridge } from "@comet/site-nextjs"; import { type FooterContentBlockData } from "@src/blocks.generated"; import { FooterContentBlock } from "@src/layout/footer/blocks/FooterContentBlock"; import { withBlockPreview } from "@src/util/blockPreview"; diff --git a/demo/site/src/app/block-preview/[domain]/[language]/main-menu/page.tsx b/demo/site/src/app/block-preview/[domain]/[language]/main-menu/page.tsx index 1261afd9cad..bb093793146 100644 --- a/demo/site/src/app/block-preview/[domain]/[language]/main-menu/page.tsx +++ b/demo/site/src/app/block-preview/[domain]/[language]/main-menu/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { useIFrameBridge } from "@comet/cms-site"; +import { useIFrameBridge } from "@comet/site-nextjs"; import { RichTextBlock } from "@src/common/blocks/RichTextBlock"; import { withBlockPreview } from "@src/util/blockPreview"; diff --git a/demo/site/src/app/block-preview/[domain]/[language]/page/page.tsx b/demo/site/src/app/block-preview/[domain]/[language]/page/page.tsx index 589d5e179dd..28bb13e20af 100644 --- a/demo/site/src/app/block-preview/[domain]/[language]/page/page.tsx +++ b/demo/site/src/app/block-preview/[domain]/[language]/page/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useBlockPreviewFetch, useIFrameBridge } from "@comet/cms-site"; +import { useBlockPreviewFetch, useIFrameBridge } from "@comet/site-nextjs"; import { type PageContentBlockData } from "@src/blocks.generated"; import { PageContentBlock } from "@src/documents/pages/blocks/PageContentBlock"; import { withBlockPreview } from "@src/util/blockPreview"; diff --git a/demo/site/src/app/block-preview/[domain]/[language]/stage/page.tsx b/demo/site/src/app/block-preview/[domain]/[language]/stage/page.tsx index 4ef50ff70e4..9f7009ce4c2 100644 --- a/demo/site/src/app/block-preview/[domain]/[language]/stage/page.tsx +++ b/demo/site/src/app/block-preview/[domain]/[language]/stage/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useIFrameBridge } from "@comet/cms-site"; +import { useIFrameBridge } from "@comet/site-nextjs"; import { StageBlock } from "@src/documents/pages/blocks/StageBlock"; import { withBlockPreview } from "@src/util/blockPreview"; diff --git a/demo/site/src/app/layout.tsx b/demo/site/src/app/layout.tsx index d0ad36ed36e..2bca40d86be 100644 --- a/demo/site/src/app/layout.tsx +++ b/demo/site/src/app/layout.tsx @@ -1,4 +1,6 @@ -import { CookieApiProvider, useLocalStorageCookieApi, useOneTrustCookieApi as useProductionCookieApi } from "@comet/cms-site"; +import "@comet/site-nextjs/css"; + +import { CookieApiProvider, useLocalStorageCookieApi, useOneTrustCookieApi as useProductionCookieApi } from "@comet/site-nextjs"; import { GlobalStyle } from "@src/app/GlobalStyle"; import { ErrorHandler } from "@src/util/ErrorHandler"; import { ResponsiveSpacingStyle } from "@src/util/ResponsiveSpacingStyle"; diff --git a/demo/site/src/app/site-preview/route.ts b/demo/site/src/app/site-preview/route.ts index ddb3a247051..f19a5840e40 100644 --- a/demo/site/src/app/site-preview/route.ts +++ b/demo/site/src/app/site-preview/route.ts @@ -1,4 +1,4 @@ -import { sitePreviewRoute } from "@comet/cms-site"; +import { sitePreviewRoute } from "@comet/site-nextjs"; import { type NextRequest } from "next/server"; export const dynamic = "force-dynamic"; diff --git a/demo/site/src/common/blocks/AccordionBlock.tsx b/demo/site/src/common/blocks/AccordionBlock.tsx index a784dd11303..47c30af2350 100644 --- a/demo/site/src/common/blocks/AccordionBlock.tsx +++ b/demo/site/src/common/blocks/AccordionBlock.tsx @@ -1,4 +1,4 @@ -import { isWithPreviewPropsData, type PropsWithData, usePreview, withPreview } from "@comet/cms-site"; +import { isWithPreviewPropsData, type PropsWithData, usePreview, withPreview } from "@comet/site-nextjs"; import { type AccordionBlockData } from "@src/blocks.generated"; import { AccordionItemBlock } from "@src/common/blocks/AccordionItemBlock"; import { PageLayout } from "@src/layout/PageLayout"; diff --git a/demo/site/src/common/blocks/AccordionItemBlock.tsx b/demo/site/src/common/blocks/AccordionItemBlock.tsx index 7eb36047de8..29766e2843d 100644 --- a/demo/site/src/common/blocks/AccordionItemBlock.tsx +++ b/demo/site/src/common/blocks/AccordionItemBlock.tsx @@ -1,4 +1,4 @@ -import { BlocksBlock, type PropsWithData, type SupportedBlocks, withPreview } from "@comet/cms-site"; +import { BlocksBlock, type PropsWithData, type SupportedBlocks, withPreview } from "@comet/site-nextjs"; import { type AccordionContentBlockData, type AccordionItemBlockData } from "@src/blocks.generated"; import { RichTextBlock } from "@src/common/blocks/RichTextBlock"; import { SpaceBlock } from "@src/common/blocks/SpaceBlock"; @@ -45,7 +45,7 @@ export const AccordionItemBlock = withPreview( - + @@ -88,13 +88,18 @@ const ContentWrapper = styled.div<{ $isExpanded: boolean }>` position: relative; display: grid; grid-template-rows: 0fr; - transition: grid-template-rows 0.5s ease-out; + transition: + grid-template-rows 0.5s ease-out, + visibility 0s linear 0.5s; + visibility: hidden; ${({ $isExpanded }) => $isExpanded && css` grid-template-rows: 1fr; padding-bottom: ${({ theme }) => theme.spacing.S300}; + visibility: visible; + transition-delay: 0s; `} `; diff --git a/demo/site/src/common/blocks/AnchorBlock.tsx b/demo/site/src/common/blocks/AnchorBlock.tsx index 87ed6feccb5..6524b19a2b2 100644 --- a/demo/site/src/common/blocks/AnchorBlock.tsx +++ b/demo/site/src/common/blocks/AnchorBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type AnchorBlockData } from "@src/blocks.generated"; export const AnchorBlock = withPreview( diff --git a/demo/site/src/common/blocks/CallToActionBlock.tsx b/demo/site/src/common/blocks/CallToActionBlock.tsx index a73be9451d0..472ae435876 100644 --- a/demo/site/src/common/blocks/CallToActionBlock.tsx +++ b/demo/site/src/common/blocks/CallToActionBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type CallToActionBlockData } from "@src/blocks.generated"; import { filesize } from "filesize"; diff --git a/demo/site/src/common/blocks/CallToActionListBlock.tsx b/demo/site/src/common/blocks/CallToActionListBlock.tsx index 35678c488f9..dd3abe7c7c7 100644 --- a/demo/site/src/common/blocks/CallToActionListBlock.tsx +++ b/demo/site/src/common/blocks/CallToActionListBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { ListBlock, type PropsWithData, withPreview } from "@comet/cms-site"; +import { ListBlock, type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type CallToActionListBlockData } from "@src/blocks.generated"; import styled from "styled-components"; @@ -22,7 +22,7 @@ const Root = styled.div` flex-flow: row wrap; gap: ${({ theme }) => theme.spacing.S300}; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { gap: ${({ theme }) => theme.spacing.S400}; } `; diff --git a/demo/site/src/common/blocks/CookieSafeYouTubeVideoBlock.tsx b/demo/site/src/common/blocks/CookieSafeYouTubeVideoBlock.tsx index c4807fd922b..0800b1b6fa6 100644 --- a/demo/site/src/common/blocks/CookieSafeYouTubeVideoBlock.tsx +++ b/demo/site/src/common/blocks/CookieSafeYouTubeVideoBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { CookieSafe, useCookieApi, YouTubeVideoBlock } from "@comet/cms-site"; +import { CookieSafe, useCookieApi, YouTubeVideoBlock } from "@comet/site-nextjs"; import { cookieIds } from "@src/util/cookieIds"; import { type ComponentProps, type CSSProperties } from "react"; import styled from "styled-components"; diff --git a/demo/site/src/common/blocks/DamImageBlock.tsx b/demo/site/src/common/blocks/DamImageBlock.tsx index 3886493ba04..264c0738776 100644 --- a/demo/site/src/common/blocks/DamImageBlock.tsx +++ b/demo/site/src/common/blocks/DamImageBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { PixelImageBlock, PreviewSkeleton, type PropsWithData, SvgImageBlock, withPreview } from "@comet/cms-site"; +import { PixelImageBlock, PreviewSkeleton, type PropsWithData, SvgImageBlock, withPreview } from "@comet/site-nextjs"; import { type DamImageBlockData, type PixelImageBlockData, type SvgImageBlockData } from "@src/blocks.generated"; import { type ImageProps as NextImageProps } from "next/image"; diff --git a/demo/site/src/common/blocks/HeadingBlock.tsx b/demo/site/src/common/blocks/HeadingBlock.tsx index 336f3838c76..2770651c363 100644 --- a/demo/site/src/common/blocks/HeadingBlock.tsx +++ b/demo/site/src/common/blocks/HeadingBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { hasRichTextBlockContent, PreviewSkeleton, type PropsWithData, withPreview } from "@comet/cms-site"; +import { hasRichTextBlockContent, PreviewSkeleton, type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type HeadingBlockData } from "@src/blocks.generated"; import { Typography } from "@src/common/components/Typography"; import { type Renderers } from "redraft"; diff --git a/demo/site/src/common/blocks/InternalLinkBlock.tsx b/demo/site/src/common/blocks/InternalLinkBlock.tsx index 6faafee3644..37fced76927 100644 --- a/demo/site/src/common/blocks/InternalLinkBlock.tsx +++ b/demo/site/src/common/blocks/InternalLinkBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData } from "@comet/cms-site"; +import { type PropsWithData } from "@comet/site-nextjs"; import { type InternalLinkBlockData } from "@src/blocks.generated"; import { type GQLPageTreeNodeScope } from "@src/graphql.generated"; import { createSitePath } from "@src/util/createSitePath"; diff --git a/demo/site/src/common/blocks/LayoutBlock.tsx b/demo/site/src/common/blocks/LayoutBlock.tsx index 4fb21e7ca38..c27b2abe5a3 100644 --- a/demo/site/src/common/blocks/LayoutBlock.tsx +++ b/demo/site/src/common/blocks/LayoutBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type LayoutBlockData } from "@src/blocks.generated"; import styled, { css } from "styled-components"; @@ -72,7 +72,7 @@ const Root = styled.div` const Box = styled.div<{ $layout: string }>` grid-column: 1 / -1; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { ${({ $layout }) => $layout === "layout1" && css` diff --git a/demo/site/src/common/blocks/LinkBlock.tsx b/demo/site/src/common/blocks/LinkBlock.tsx index 60124890a46..428640d09e3 100644 --- a/demo/site/src/common/blocks/LinkBlock.tsx +++ b/demo/site/src/common/blocks/LinkBlock.tsx @@ -8,7 +8,7 @@ import { type PropsWithData, type SupportedBlocks, withPreview, -} from "@comet/cms-site"; +} from "@comet/site-nextjs"; import { type LinkBlockData } from "@src/blocks.generated"; import { NewsLinkBlock } from "@src/news/blocks/NewsLinkBlock"; import { type PropsWithChildren } from "react"; diff --git a/demo/site/src/common/blocks/MediaBlock.tsx b/demo/site/src/common/blocks/MediaBlock.tsx index 3c9c8776748..bee9b284cea 100644 --- a/demo/site/src/common/blocks/MediaBlock.tsx +++ b/demo/site/src/common/blocks/MediaBlock.tsx @@ -1,4 +1,12 @@ -import { DamVideoBlock, OneOfBlock, PreviewSkeleton, type PropsWithData, type SupportedBlocks, VimeoVideoBlock, withPreview } from "@comet/cms-site"; +import { + DamVideoBlock, + OneOfBlock, + PreviewSkeleton, + type PropsWithData, + type SupportedBlocks, + VimeoVideoBlock, + withPreview, +} from "@comet/site-nextjs"; import { type MediaBlockData } from "@src/blocks.generated"; import { DamImageBlock } from "@src/common/blocks/DamImageBlock"; diff --git a/demo/site/src/common/blocks/MediaGalleryBlock.tsx b/demo/site/src/common/blocks/MediaGalleryBlock.tsx index 81048396364..18035f4e4f4 100644 --- a/demo/site/src/common/blocks/MediaGalleryBlock.tsx +++ b/demo/site/src/common/blocks/MediaGalleryBlock.tsx @@ -1,7 +1,7 @@ import "swiper/css"; import "swiper/css/navigation"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type MediaGalleryBlockData } from "@src/blocks.generated"; import { MediaBlock } from "@src/common/blocks/MediaBlock"; import { Typography } from "@src/common/components/Typography"; @@ -53,15 +53,15 @@ const PageLayoutContent = styled.div` grid-column: 2 / -2; position: relative; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { grid-column: 5 / -5; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { grid-column: 6 / -6; } - ${({ theme }) => theme.breakpoints.lg.mediaQuery} { + ${({ theme }) => theme.breakpoints.xl.mediaQuery} { grid-column: 7 / -7; } `; diff --git a/demo/site/src/common/blocks/RichTextBlock.tsx b/demo/site/src/common/blocks/RichTextBlock.tsx index bc3a73003ad..9f20c2f9959 100644 --- a/demo/site/src/common/blocks/RichTextBlock.tsx +++ b/demo/site/src/common/blocks/RichTextBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { hasRichTextBlockContent, PreviewSkeleton, type PropsWithData, withPreview } from "@comet/cms-site"; +import { hasRichTextBlockContent, PreviewSkeleton, type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type LinkBlockData, type RichTextBlockData } from "@src/blocks.generated"; import { PageLayout } from "@src/layout/PageLayout"; import redraft, { type Renderers, type TextBlockRenderFn } from "redraft"; @@ -117,7 +117,7 @@ const DisableLastBottomSpacing = styled.div` > *:last-child { margin-bottom: 0; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { margin-bottom: 0; } } diff --git a/demo/site/src/common/blocks/SpaceBlock.tsx b/demo/site/src/common/blocks/SpaceBlock.tsx index 352e7c2df8b..f9e9ffd8528 100644 --- a/demo/site/src/common/blocks/SpaceBlock.tsx +++ b/demo/site/src/common/blocks/SpaceBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type SpaceBlockData } from "@src/blocks.generated"; import styled from "styled-components"; diff --git a/demo/site/src/common/blocks/StandaloneCallToActionListBlock.tsx b/demo/site/src/common/blocks/StandaloneCallToActionListBlock.tsx index 3b5753d504e..34e84447bce 100644 --- a/demo/site/src/common/blocks/StandaloneCallToActionListBlock.tsx +++ b/demo/site/src/common/blocks/StandaloneCallToActionListBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type StandaloneCallToActionListBlockData } from "@src/blocks.generated"; import { PageLayout } from "@src/layout/PageLayout"; import { type CSSProperties } from "react"; diff --git a/demo/site/src/common/blocks/StandaloneHeadingBlock.tsx b/demo/site/src/common/blocks/StandaloneHeadingBlock.tsx index eb28e78f979..13e5d08b45d 100644 --- a/demo/site/src/common/blocks/StandaloneHeadingBlock.tsx +++ b/demo/site/src/common/blocks/StandaloneHeadingBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type StandaloneHeadingBlockData } from "@src/blocks.generated"; import { PageLayout } from "@src/layout/PageLayout"; import { type CSSProperties } from "react"; diff --git a/demo/site/src/common/blocks/StandaloneMediaBlock.tsx b/demo/site/src/common/blocks/StandaloneMediaBlock.tsx index 25687db848f..90a3b8c5fff 100644 --- a/demo/site/src/common/blocks/StandaloneMediaBlock.tsx +++ b/demo/site/src/common/blocks/StandaloneMediaBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type StandaloneMediaBlockData } from "@src/blocks.generated"; import { PageLayout } from "@src/layout/PageLayout"; diff --git a/demo/site/src/common/blocks/TextImageBlock.tsx b/demo/site/src/common/blocks/TextImageBlock.tsx index 79592935358..7e771bbaa5d 100644 --- a/demo/site/src/common/blocks/TextImageBlock.tsx +++ b/demo/site/src/common/blocks/TextImageBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type TextImageBlockData } from "@src/blocks.generated"; import styled, { css } from "styled-components"; diff --git a/demo/site/src/common/components/Breadcrumbs.fragment.ts b/demo/site/src/common/components/Breadcrumbs.fragment.ts index 826fd5febd0..36efb673bc3 100644 --- a/demo/site/src/common/components/Breadcrumbs.fragment.ts +++ b/demo/site/src/common/components/Breadcrumbs.fragment.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; export const breadcrumbsFragment = gql` fragment Breadcrumbs on PageTreeNode { diff --git a/demo/site/src/common/components/Typography.tsx b/demo/site/src/common/components/Typography.tsx index ac817c22a5c..913fb50a4a9 100644 --- a/demo/site/src/common/components/Typography.tsx +++ b/demo/site/src/common/components/Typography.tsx @@ -209,7 +209,7 @@ export const Typography = styled.div css` margin-bottom: 0; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { margin-bottom: 0; } `}; diff --git a/demo/site/src/common/helpers/CookiePlaceholders.tsx b/demo/site/src/common/helpers/CookiePlaceholders.tsx index fa44077aea1..5607f0b76ce 100644 --- a/demo/site/src/common/helpers/CookiePlaceholders.tsx +++ b/demo/site/src/common/helpers/CookiePlaceholders.tsx @@ -1,4 +1,4 @@ -import { useCookieApi } from "@comet/cms-site"; +import { useCookieApi } from "@comet/site-nextjs"; import styled from "styled-components"; export const LoadingCookiePlaceholder = () => ( diff --git a/demo/site/src/common/helpers/HiddenIfInvalidLink.tsx b/demo/site/src/common/helpers/HiddenIfInvalidLink.tsx index 297559fe274..e5c206b1945 100644 --- a/demo/site/src/common/helpers/HiddenIfInvalidLink.tsx +++ b/demo/site/src/common/helpers/HiddenIfInvalidLink.tsx @@ -1,4 +1,4 @@ -import { usePreview } from "@comet/cms-site"; +import { usePreview } from "@comet/site-nextjs"; import { type DamFileDownloadLinkBlockData, type EmailLinkBlockData, diff --git a/demo/site/src/common/helpers/SvgUse.tsx b/demo/site/src/common/helpers/SvgUse.tsx index f2267c975c5..64c5008b024 100644 --- a/demo/site/src/common/helpers/SvgUse.tsx +++ b/demo/site/src/common/helpers/SvgUse.tsx @@ -6,7 +6,7 @@ interface SvgUseProps extends SVGProps { } export const SvgUse = ({ href, ...props }: SvgUseProps) => ( - + ); diff --git a/demo/site/src/documents/links/Link.tsx b/demo/site/src/documents/links/Link.tsx index 54c09cf89b7..b64f68b6aae 100644 --- a/demo/site/src/documents/links/Link.tsx +++ b/demo/site/src/documents/links/Link.tsx @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { type DamFileDownloadLinkBlockData, type ExternalLinkBlockData, type InternalLinkBlockData } from "@src/blocks.generated"; import { type GQLPageTreeNodeScopeInput } from "@src/graphql.generated"; import { createGraphQLFetch } from "@src/util/graphQLClient"; diff --git a/demo/site/src/documents/pages/Page.tsx b/demo/site/src/documents/pages/Page.tsx index 060630206d7..9864f587ecd 100644 --- a/demo/site/src/documents/pages/Page.tsx +++ b/demo/site/src/documents/pages/Page.tsx @@ -1,4 +1,4 @@ -import { generateImageUrl, gql } from "@comet/cms-site"; +import { generateImageUrl, gql } from "@comet/site-nextjs"; import Breadcrumbs from "@src/common/components/Breadcrumbs"; import { breadcrumbsFragment } from "@src/common/components/Breadcrumbs.fragment"; import { type GQLPageTreeNodeScopeInput } from "@src/graphql.generated"; @@ -8,6 +8,7 @@ import { TopNavigation } from "@src/layout/topNavigation/TopNavigation"; import { topMenuPageTreeNodeFragment } from "@src/layout/topNavigation/TopNavigation.fragment"; import { createGraphQLFetch } from "@src/util/graphQLClient"; import { recursivelyLoadBlockData } from "@src/util/recursivelyLoadBlockData"; +import { getSiteConfigForDomain } from "@src/util/siteConfig"; import { type Metadata, type ResolvingMetadata } from "next"; import { notFound } from "next/navigation"; @@ -76,12 +77,15 @@ async function fetchData({ pageTreeNodeId, scope }: Props) { export async function generateMetadata({ pageTreeNodeId, scope }: Props, parent: ResolvingMetadata): Promise { const data = await fetchData({ pageTreeNodeId, scope }); + const siteConfig = getSiteConfigForDomain(scope.domain); + const document = data?.pageContent?.document; if (!document) { return {}; } - const siteUrl = "http://localhost:3000"; //TODO get from site config - const canonicalUrl = document.seo.canonicalUrl || `${siteUrl}${data.pageContent.path}`; + + const siteUrl = siteConfig.url; + const canonicalUrl = (document.seo.canonicalUrl || `${siteUrl}/${scope.language}${data.pageContent.path}`).replace(/\/$/, ""); // Remove trailing slash for "home" // TODO move into library return { diff --git a/demo/site/src/documents/pages/blocks/BasicStageBlock.tsx b/demo/site/src/documents/pages/blocks/BasicStageBlock.tsx index 5690d889911..fbe89715092 100644 --- a/demo/site/src/documents/pages/blocks/BasicStageBlock.tsx +++ b/demo/site/src/documents/pages/blocks/BasicStageBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type BasicStageBlockData } from "@src/blocks.generated"; import { CallToActionListBlock } from "@src/common/blocks/CallToActionListBlock"; import { HeadingBlock } from "@src/common/blocks/HeadingBlock"; @@ -73,7 +73,7 @@ const Content = styled.div<{ $alignItems: CSSProperties["alignItems"] }>` const MediaPhone = styled.div` height: 800px; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: none; } `; @@ -82,11 +82,11 @@ const MediaTablet = styled.div` display: none; height: 700px; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: none; } `; @@ -95,11 +95,11 @@ const MediaTabletLandscape = styled.div` display: none; height: 650px; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: none; } `; @@ -108,11 +108,11 @@ const MediaDesktop = styled.div` display: none; height: 750px; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.lg.mediaQuery} { + ${({ theme }) => theme.breakpoints.xl.mediaQuery} { height: 800px; } `; diff --git a/demo/site/src/documents/pages/blocks/BillboardTeaserBlock.tsx b/demo/site/src/documents/pages/blocks/BillboardTeaserBlock.tsx index 021ef4e2aeb..35a893416c9 100644 --- a/demo/site/src/documents/pages/blocks/BillboardTeaserBlock.tsx +++ b/demo/site/src/documents/pages/blocks/BillboardTeaserBlock.tsx @@ -1,4 +1,4 @@ -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type BillboardTeaserBlockData } from "@src/blocks.generated"; import { CallToActionListBlock } from "@src/common/blocks/CallToActionListBlock"; import { HeadingBlock } from "@src/common/blocks/HeadingBlock"; @@ -69,18 +69,18 @@ const Content = styled.div` ${({ theme }) => css` grid-column: 3 / -3; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { grid-column: 5 / -5; } - ${theme.breakpoints.lg.mediaQuery} { + ${theme.breakpoints.xl.mediaQuery} { grid-column: 7 / -7; } `}; `; const ImageMobile = styled.div` - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: none; } `; @@ -88,11 +88,11 @@ const ImageMobile = styled.div` const ImageTablet = styled.div` display: none; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: none; } `; @@ -100,11 +100,11 @@ const ImageTablet = styled.div` const ImageDesktop = styled.div` display: none; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { display: block; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: none; } `; @@ -112,7 +112,7 @@ const ImageDesktop = styled.div` const ImageLargeDesktop = styled.div` display: none; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: block; } `; diff --git a/demo/site/src/documents/pages/blocks/ColumnsBlock.tsx b/demo/site/src/documents/pages/blocks/ColumnsBlock.tsx index 625c4336d58..2ec958fddf8 100644 --- a/demo/site/src/documents/pages/blocks/ColumnsBlock.tsx +++ b/demo/site/src/documents/pages/blocks/ColumnsBlock.tsx @@ -1,4 +1,4 @@ -import { BlocksBlock, type PropsWithData, type SupportedBlocks, withPreview } from "@comet/cms-site"; +import { BlocksBlock, type PropsWithData, type SupportedBlocks, withPreview } from "@comet/site-nextjs"; import { type ColumnsBlockData, type ColumnsContentBlockData } from "@src/blocks.generated"; import { AccordionBlock } from "@src/common/blocks/AccordionBlock"; import { AnchorBlock } from "@src/common/blocks/AnchorBlock"; @@ -50,21 +50,21 @@ const Column = styled.div<{ $layout: string }>` css` grid-column: 5 / -5; - ${theme.breakpoints.xs.mediaQuery} { + ${theme.breakpoints.sm.mediaQuery} { grid-column: 7 / -7; } - ${theme.breakpoints.sm.mediaQuery} { + ${theme.breakpoints.md.mediaQuery} { grid-column: 8 / -8; } - ${theme.breakpoints.md.mediaQuery} { + ${theme.breakpoints.lg.mediaQuery} { grid-column: 9 / -9; } - ${theme.breakpoints.lg.mediaQuery} { + ${theme.breakpoints.xl.mediaQuery} { grid-column: 10 / -10; } `}; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { ${({ $layout }) => $layout === "4-16-4" && css` diff --git a/demo/site/src/documents/pages/blocks/ContentGroupBlock.tsx b/demo/site/src/documents/pages/blocks/ContentGroupBlock.tsx index fc062c9ed7a..4f29a226a3e 100644 --- a/demo/site/src/documents/pages/blocks/ContentGroupBlock.tsx +++ b/demo/site/src/documents/pages/blocks/ContentGroupBlock.tsx @@ -1,4 +1,4 @@ -import { BlocksBlock, type PropsWithData, type SupportedBlocks, withPreview } from "@comet/cms-site"; +import { BlocksBlock, type PropsWithData, type SupportedBlocks, withPreview } from "@comet/site-nextjs"; import { type ContentGroupBlockData, type ContentGroupContentBlockData } from "@src/blocks.generated"; import { PageContentAccordionBlock } from "@src/common/blocks/AccordionBlock"; import { AnchorBlock } from "@src/common/blocks/AnchorBlock"; diff --git a/demo/site/src/documents/pages/blocks/FullWidthImageBlock.tsx b/demo/site/src/documents/pages/blocks/FullWidthImageBlock.tsx index 06a2eab80ac..baf8a493af3 100644 --- a/demo/site/src/documents/pages/blocks/FullWidthImageBlock.tsx +++ b/demo/site/src/documents/pages/blocks/FullWidthImageBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { OptionalBlock, type PropsWithData, withPreview } from "@comet/cms-site"; +import { OptionalBlock, type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type FullWidthImageBlockData } from "@src/blocks.generated"; import { DamImageBlock } from "@src/common/blocks/DamImageBlock"; import { RichTextBlock } from "@src/common/blocks/RichTextBlock"; diff --git a/demo/site/src/documents/pages/blocks/KeyFactItemBlock.tsx b/demo/site/src/documents/pages/blocks/KeyFactItemBlock.tsx index 00110903348..ed930cf9af5 100644 --- a/demo/site/src/documents/pages/blocks/KeyFactItemBlock.tsx +++ b/demo/site/src/documents/pages/blocks/KeyFactItemBlock.tsx @@ -1,4 +1,4 @@ -import { hasRichTextBlockContent, type PropsWithData, SvgImageBlock, withPreview } from "@comet/cms-site"; +import { hasRichTextBlockContent, type PropsWithData, SvgImageBlock, withPreview } from "@comet/site-nextjs"; import { type KeyFactsItemBlockData } from "@src/blocks.generated"; import { defaultRichTextInlineStyleMap, RichTextBlock } from "@src/common/blocks/RichTextBlock"; import { Typography } from "@src/common/components/Typography"; diff --git a/demo/site/src/documents/pages/blocks/KeyFactsBlock.tsx b/demo/site/src/documents/pages/blocks/KeyFactsBlock.tsx index 62b5d432d44..778356292fc 100644 --- a/demo/site/src/documents/pages/blocks/KeyFactsBlock.tsx +++ b/demo/site/src/documents/pages/blocks/KeyFactsBlock.tsx @@ -1,4 +1,4 @@ -import { ListBlock, type PropsWithData, withPreview } from "@comet/cms-site"; +import { ListBlock, type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type KeyFactsBlockData } from "@src/blocks.generated"; import { PageLayout } from "@src/layout/PageLayout"; import styled, { css } from "styled-components"; @@ -31,7 +31,7 @@ const ItemWrapper = styled.div<{ $listItemCount: number }>` css` grid-template-columns: repeat(${Math.min($listItemCount, 2)}, 1fr); - ${theme.breakpoints.sm.mediaQuery} { + ${theme.breakpoints.md.mediaQuery} { grid-template-columns: repeat(${Math.min($listItemCount, 4)}, 1fr); } `} diff --git a/demo/site/src/documents/pages/blocks/PageContentBlock.tsx b/demo/site/src/documents/pages/blocks/PageContentBlock.tsx index 948708fe515..6ba04a1adce 100644 --- a/demo/site/src/documents/pages/blocks/PageContentBlock.tsx +++ b/demo/site/src/documents/pages/blocks/PageContentBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { BlocksBlock, type PropsWithData, type SupportedBlocks } from "@comet/cms-site"; +import { BlocksBlock, type PropsWithData, type SupportedBlocks } from "@comet/site-nextjs"; import { type PageContentBlockData } from "@src/blocks.generated"; import { PageContentAccordionBlock } from "@src/common/blocks/AccordionBlock"; import { AnchorBlock } from "@src/common/blocks/AnchorBlock"; diff --git a/demo/site/src/documents/pages/blocks/SliderBlock.tsx b/demo/site/src/documents/pages/blocks/SliderBlock.tsx index d303791076e..06fa320e449 100644 --- a/demo/site/src/documents/pages/blocks/SliderBlock.tsx +++ b/demo/site/src/documents/pages/blocks/SliderBlock.tsx @@ -1,4 +1,4 @@ -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type SliderBlockData } from "@src/blocks.generated"; import { MediaBlock } from "@src/common/blocks/MediaBlock"; import { RichTextBlock } from "@src/common/blocks/RichTextBlock"; diff --git a/demo/site/src/documents/pages/blocks/StageBlock.tsx b/demo/site/src/documents/pages/blocks/StageBlock.tsx index 32840234114..a910f998f37 100644 --- a/demo/site/src/documents/pages/blocks/StageBlock.tsx +++ b/demo/site/src/documents/pages/blocks/StageBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { ListBlock, type PropsWithData } from "@comet/cms-site"; +import { ListBlock, type PropsWithData } from "@comet/site-nextjs"; import { type StageBlockData } from "@src/blocks.generated"; import { BasicStageBlock } from "./BasicStageBlock"; diff --git a/demo/site/src/documents/pages/blocks/TeaserBlock.tsx b/demo/site/src/documents/pages/blocks/TeaserBlock.tsx index ddc3a801fa4..08fc234a01c 100644 --- a/demo/site/src/documents/pages/blocks/TeaserBlock.tsx +++ b/demo/site/src/documents/pages/blocks/TeaserBlock.tsx @@ -1,4 +1,4 @@ -import { ListBlock, type PropsWithData, withPreview } from "@comet/cms-site"; +import { ListBlock, type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type TeaserBlockData } from "@src/blocks.generated"; import { PageLayout } from "@src/layout/PageLayout"; import styled, { css } from "styled-components"; @@ -27,7 +27,7 @@ const ItemWrapper = styled.div` gap: ${({ theme }) => theme.spacing.D100}; ${({ theme }) => css` - ${theme.breakpoints.sm.mediaQuery} { + ${theme.breakpoints.md.mediaQuery} { grid-template-columns: repeat(4, 1fr); } `} diff --git a/demo/site/src/documents/pages/blocks/TeaserItemBlock.tsx b/demo/site/src/documents/pages/blocks/TeaserItemBlock.tsx index 794c98b43f4..4f3d670a133 100644 --- a/demo/site/src/documents/pages/blocks/TeaserItemBlock.tsx +++ b/demo/site/src/documents/pages/blocks/TeaserItemBlock.tsx @@ -1,4 +1,4 @@ -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type TeaserItemBlockData } from "@src/blocks.generated"; import { LinkBlock } from "@src/common/blocks/LinkBlock"; import { MediaBlock } from "@src/common/blocks/MediaBlock"; @@ -45,7 +45,7 @@ const Link = styled(LinkBlock)` gap: ${({ theme }) => theme.spacing.S300}; color: ${({ theme }) => theme.palette.text.primary}; - ${({ theme }) => theme.breakpoints.sm.mediaQuery} { + ${({ theme }) => theme.breakpoints.md.mediaQuery} { flex: unset; gap: ${({ theme }) => theme.spacing.S400}; flex-direction: column; @@ -55,7 +55,7 @@ const Link = styled(LinkBlock)` const MediaMobile = styled.div` flex: 1; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: none; } `; @@ -64,7 +64,7 @@ const MediaDesktop = styled.div` flex: 1; display: none; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { display: block; } `; diff --git a/demo/site/src/layout/footer/Footer.fragment.ts b/demo/site/src/layout/footer/Footer.fragment.ts index bb4b0c70761..ad484aa1f46 100644 --- a/demo/site/src/layout/footer/Footer.fragment.ts +++ b/demo/site/src/layout/footer/Footer.fragment.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; export const footerFragment = gql` fragment Footer on Footer { diff --git a/demo/site/src/layout/footer/blocks/FooterContentBlock.tsx b/demo/site/src/layout/footer/blocks/FooterContentBlock.tsx index 8a4e88a9f9e..fde2ddd3152 100644 --- a/demo/site/src/layout/footer/blocks/FooterContentBlock.tsx +++ b/demo/site/src/layout/footer/blocks/FooterContentBlock.tsx @@ -1,4 +1,4 @@ -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type FooterContentBlockData } from "@src/blocks.generated"; import { DamImageBlock } from "@src/common/blocks/DamImageBlock"; import { LinkBlock } from "@src/common/blocks/LinkBlock"; @@ -55,7 +55,7 @@ const PageLayoutContent = styled.div` align-items: center; padding: ${({ theme }) => `${theme.spacing.D400} 0`}; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { position: relative; gap: ${({ theme }) => theme.spacing.D100}; flex-direction: row; @@ -70,13 +70,13 @@ const TopContainer = styled.div` align-items: center; gap: ${({ theme }) => theme.spacing.D100}; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { align-self: stretch; flex-direction: row-reverse; justify-content: space-between; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { flex-direction: row; } `; @@ -85,11 +85,11 @@ const RichTextWrapper = styled.div` width: 100%; text-align: center; - ${({ theme }) => theme.breakpoints.xs.mediaQuery} { + ${({ theme }) => theme.breakpoints.sm.mediaQuery} { text-align: left; } - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { max-width: 80%; } `; @@ -97,7 +97,7 @@ const RichTextWrapper = styled.div` const ImageWrapper = styled.div` width: 100px; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { position: absolute; width: 100%; max-width: 100px; @@ -114,7 +114,7 @@ const LinkCopyrightWrapper = styled.div` align-items: center; gap: ${({ theme }) => theme.spacing.S500}; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { width: 80%; align-items: flex-end; } @@ -130,7 +130,7 @@ const LinksWrapper = styled.div` const CopyrightNotice = styled(Typography)` text-align: center; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { text-align: right; } `; @@ -148,7 +148,7 @@ const HorizontalLine = styled.hr` color: ${({ theme }) => theme.palette.gray["600"]}; margin: ${({ theme }) => `${theme.spacing.D300} 0`}; - ${({ theme }) => theme.breakpoints.md.mediaQuery} { + ${({ theme }) => theme.breakpoints.lg.mediaQuery} { display: none; } `; diff --git a/demo/site/src/layout/header/Header.fragment.ts b/demo/site/src/layout/header/Header.fragment.ts index f3b42675a6f..57eb67f4fda 100644 --- a/demo/site/src/layout/header/Header.fragment.ts +++ b/demo/site/src/layout/header/Header.fragment.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { pageLinkFragment } from "./PageLink.fragment"; diff --git a/demo/site/src/layout/header/PageLink.fragment.ts b/demo/site/src/layout/header/PageLink.fragment.ts index 8e3be6d94f8..40e5bbfafd6 100644 --- a/demo/site/src/layout/header/PageLink.fragment.ts +++ b/demo/site/src/layout/header/PageLink.fragment.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; export const pageLinkFragment = gql` fragment PageLink on PageTreeNode { diff --git a/demo/site/src/layout/topNavigation/TopNavigation.fragment.ts b/demo/site/src/layout/topNavigation/TopNavigation.fragment.ts index 8de109f0b82..dd31d2399fc 100644 --- a/demo/site/src/layout/topNavigation/TopNavigation.fragment.ts +++ b/demo/site/src/layout/topNavigation/TopNavigation.fragment.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; export const topMenuPageTreeNodeFragment = gql` fragment TopMenuPageTreeNode on PageTreeNode { diff --git a/demo/site/src/middleware.ts b/demo/site/src/middleware.ts index 7692e39e144..b9a4d7e5572 100644 --- a/demo/site/src/middleware.ts +++ b/demo/site/src/middleware.ts @@ -6,6 +6,7 @@ import { withDomainRewriteMiddleware } from "./middleware/domainRewrite"; import { withPredefinedPagesMiddleware } from "./middleware/predefinedPages"; import { withPreviewMiddleware } from "./middleware/preview"; import { withRedirectToMainHostMiddleware } from "./middleware/redirectToMainHost"; +import { withRobotsMiddleware } from "./middleware/robots"; import { withSkipRewriteMiddleware } from "./middleware/skipRewrite"; import { withStatusMiddleware } from "./middleware/status"; @@ -17,6 +18,7 @@ export default chain([ withCspHeadersMiddleware, withPreviewMiddleware, withRedirectToMainHostMiddleware, + withRobotsMiddleware, // for robots.txt, the middleware may only be skipped after the main host redirect withPredefinedPagesMiddleware, withDomainRewriteMiddleware, // must be last (rewrites all urls) ]); diff --git a/demo/site/src/middleware/cspHeaders.ts b/demo/site/src/middleware/cspHeaders.ts index 0bd16a8fded..95b3881d1a7 100644 --- a/demo/site/src/middleware/cspHeaders.ts +++ b/demo/site/src/middleware/cspHeaders.ts @@ -20,7 +20,7 @@ export function withCspHeadersMiddleware(middleware: CustomMiddleware) { frame-ancestors ${process.env.ADMIN_URL ?? "none"}; upgrade-insecure-requests; block-all-mixed-content; - frame-src 'self' https://*.youtube.com https://*.youtube-nocookie.com; + frame-src 'self' https://*.youtube-nocookie.com https://player.vimeo.com; ` .replace(/\s{2,}/g, " ") .trim(), diff --git a/demo/site/src/middleware/domainRewrite.ts b/demo/site/src/middleware/domainRewrite.ts index 7c25108a57e..4b4062dfe3c 100644 --- a/demo/site/src/middleware/domainRewrite.ts +++ b/demo/site/src/middleware/domainRewrite.ts @@ -1,4 +1,4 @@ -import { previewParams } from "@comet/cms-site"; +import { previewParams } from "@comet/site-nextjs"; import { getHostByHeaders, getSiteConfigForHost } from "@src/util/siteConfig"; import { type NextRequest, NextResponse } from "next/server"; diff --git a/demo/site/src/middleware/predefinedPages.ts b/demo/site/src/middleware/predefinedPages.ts index b04b3094f64..ab9db335d73 100644 --- a/demo/site/src/middleware/predefinedPages.ts +++ b/demo/site/src/middleware/predefinedPages.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { predefinedPagePaths } from "@src/documents/predefinedPages/predefinedPagePaths"; import { createGraphQLFetchMiddleware } from "@src/util/graphQLClientMiddleware"; import { getHostByHeaders, getSiteConfigForDomain, getSiteConfigForHost } from "@src/util/siteConfig"; diff --git a/demo/site/src/middleware/robots.ts b/demo/site/src/middleware/robots.ts new file mode 100644 index 00000000000..06b2c76c512 --- /dev/null +++ b/demo/site/src/middleware/robots.ts @@ -0,0 +1,13 @@ +import { type NextRequest, NextResponse } from "next/server"; + +import { type CustomMiddleware } from "./chain"; + +export function withRobotsMiddleware(middleware: CustomMiddleware) { + return async (request: NextRequest) => { + if (request.nextUrl.pathname === "/robots.txt") { + // don't apply any other middlewares + return NextResponse.next(); + } + return middleware(request); + }; +} diff --git a/demo/site/src/middleware/skipRewrite.ts b/demo/site/src/middleware/skipRewrite.ts index 6da4d1e42cd..a49d1dd8180 100644 --- a/demo/site/src/middleware/skipRewrite.ts +++ b/demo/site/src/middleware/skipRewrite.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { type CustomMiddleware } from "./chain"; export function withSkipRewriteMiddleware(middleware: CustomMiddleware) { - const skipFiles = ["/favicon.ico", "/apple-icon.png", "/icon.svg", "/manifest.json", "/robots.txt"]; + const skipFiles = ["/favicon.ico", "/apple-icon.png", "/icon.svg", "/manifest.json"]; const skipPaths = ["/_next/static/", "/_next/image/", "/assets/"]; return async (request: NextRequest) => { diff --git a/demo/site/src/middleware/status.ts b/demo/site/src/middleware/status.ts index 10077d99564..2dd9d316361 100644 --- a/demo/site/src/middleware/status.ts +++ b/demo/site/src/middleware/status.ts @@ -5,7 +5,7 @@ import { type CustomMiddleware } from "./chain"; export function withStatusMiddleware(middleware: CustomMiddleware) { return async (request: NextRequest) => { if (request.nextUrl.pathname === "/api/status") { - return NextResponse.json({ status: "OK" }); + return NextResponse.json({ status: "OK" }, { headers: { "cache-control": "no-store" } }); } return middleware(request); }; diff --git a/demo/site/src/news/NewsPage.loader.ts b/demo/site/src/news/NewsPage.loader.ts index 6b1b3a2ce5f..d0a0909cafb 100644 --- a/demo/site/src/news/NewsPage.loader.ts +++ b/demo/site/src/news/NewsPage.loader.ts @@ -1,4 +1,4 @@ -import { gql } from "@comet/cms-site"; +import { gql } from "@comet/site-nextjs"; import { type GQLNewsContentScopeInput } from "@src/graphql.generated"; import { createGraphQLFetch } from "@src/util/graphQLClient"; diff --git a/demo/site/src/news/blocks/NewsContentBlock.tsx b/demo/site/src/news/blocks/NewsContentBlock.tsx index a63632b4c43..4b00cc887be 100644 --- a/demo/site/src/news/blocks/NewsContentBlock.tsx +++ b/demo/site/src/news/blocks/NewsContentBlock.tsx @@ -1,4 +1,4 @@ -import { BlocksBlock, type PropsWithData, type SupportedBlocks } from "@comet/cms-site"; +import { BlocksBlock, type PropsWithData, type SupportedBlocks } from "@comet/site-nextjs"; import { type NewsContentBlockData } from "@src/blocks.generated"; import { DamImageBlock } from "@src/common/blocks/DamImageBlock"; import { HeadingBlock } from "@src/common/blocks/HeadingBlock"; diff --git a/demo/site/src/news/blocks/NewsDetailBlock.loader.ts b/demo/site/src/news/blocks/NewsDetailBlock.loader.ts index f3f4fe10443..0fab3e8612e 100644 --- a/demo/site/src/news/blocks/NewsDetailBlock.loader.ts +++ b/demo/site/src/news/blocks/NewsDetailBlock.loader.ts @@ -1,4 +1,4 @@ -import { type BlockLoader, gql } from "@comet/cms-site"; +import { type BlockLoader, gql } from "@comet/site-nextjs"; import { type NewsLinkBlockData } from "@src/blocks.generated"; import { type GQLNewsBlockDetailQuery, type GQLNewsBlockDetailQueryVariables } from "./NewsDetailBlock.loader.generated"; diff --git a/demo/site/src/news/blocks/NewsDetailBlock.tsx b/demo/site/src/news/blocks/NewsDetailBlock.tsx index 22452bcc0b3..7712f6efac6 100644 --- a/demo/site/src/news/blocks/NewsDetailBlock.tsx +++ b/demo/site/src/news/blocks/NewsDetailBlock.tsx @@ -1,5 +1,5 @@ "use client"; -import { type PropsWithData } from "@comet/cms-site"; +import { type PropsWithData } from "@comet/site-nextjs"; import { type NewsLinkBlockData } from "@src/blocks.generated"; import { type PropsWithChildren } from "react"; diff --git a/demo/site/src/news/blocks/NewsLinkBlock.tsx b/demo/site/src/news/blocks/NewsLinkBlock.tsx index 7795c5be41d..976605b880d 100644 --- a/demo/site/src/news/blocks/NewsLinkBlock.tsx +++ b/demo/site/src/news/blocks/NewsLinkBlock.tsx @@ -1,4 +1,4 @@ -import { type PropsWithData } from "@comet/cms-site"; +import { type PropsWithData } from "@comet/site-nextjs"; import { type NewsLinkBlockData } from "@src/blocks.generated"; import { createSitePath } from "@src/util/createSitePath"; import Link from "next/link"; diff --git a/demo/site/src/news/blocks/NewsListBlock.loader.ts b/demo/site/src/news/blocks/NewsListBlock.loader.ts index 4a8a197b5da..56f18d0b6b0 100644 --- a/demo/site/src/news/blocks/NewsListBlock.loader.ts +++ b/demo/site/src/news/blocks/NewsListBlock.loader.ts @@ -1,4 +1,4 @@ -import { type BlockLoader, gql } from "@comet/cms-site"; +import { type BlockLoader, gql } from "@comet/site-nextjs"; import { type NewsListBlockData } from "@src/blocks.generated"; import { type GQLNewsListBlockNewsFragment, type GQLNewsListBlockQuery, type GQLNewsListBlockQueryVariables } from "./NewsListBlock.loader.generated"; diff --git a/demo/site/src/news/blocks/NewsListBlock.tsx b/demo/site/src/news/blocks/NewsListBlock.tsx index b30a3b9f6e4..aaa356cddc2 100644 --- a/demo/site/src/news/blocks/NewsListBlock.tsx +++ b/demo/site/src/news/blocks/NewsListBlock.tsx @@ -1,4 +1,4 @@ -import { type PropsWithData, withPreview } from "@comet/cms-site"; +import { type PropsWithData, withPreview } from "@comet/site-nextjs"; import { type NewsListBlockData } from "@src/blocks.generated"; import { createSitePath } from "@src/util/createSitePath"; import Link from "next/link"; diff --git a/demo/site/src/theme.ts b/demo/site/src/theme.ts index 34774e70f2e..a849b3b4844 100644 --- a/demo/site/src/theme.ts +++ b/demo/site/src/theme.ts @@ -57,10 +57,10 @@ export const theme = { }, fontFamily: "Arial, sans-serif", breakpoints: { - xs: createBreakpoint(600), - sm: createBreakpoint(900), - md: createBreakpoint(1200), - lg: createBreakpoint(1600), + sm: createBreakpoint(600), + md: createBreakpoint(900), + lg: createBreakpoint(1200), + xl: createBreakpoint(1600), }, spacing: { D100: "var(--spacing-d100)", diff --git a/demo/site/src/util/ErrorHandler.tsx b/demo/site/src/util/ErrorHandler.tsx index 8b00895f5d8..52be3201bb7 100644 --- a/demo/site/src/util/ErrorHandler.tsx +++ b/demo/site/src/util/ErrorHandler.tsx @@ -1,6 +1,6 @@ "use client"; -import { ErrorHandlerProvider } from "@comet/cms-site"; +import { ErrorHandlerProvider } from "@comet/site-nextjs"; import { type ErrorInfo, type PropsWithChildren } from "react"; export function ErrorHandler({ children }: PropsWithChildren) { diff --git a/demo/site/src/util/blockPreview.tsx b/demo/site/src/util/blockPreview.tsx index 96c014fe418..91cbf809717 100644 --- a/demo/site/src/util/blockPreview.tsx +++ b/demo/site/src/util/blockPreview.tsx @@ -1,6 +1,6 @@ "use client"; -import { BlockPreviewProvider, IFrameBridgeProvider } from "@comet/cms-site"; +import { BlockPreviewProvider, IFrameBridgeProvider } from "@comet/site-nextjs"; import { type FunctionComponent } from "react"; export const withBlockPreview = (Component: FunctionComponent) => () => { diff --git a/demo/site/src/util/graphQLClient.ts b/demo/site/src/util/graphQLClient.ts index 7cf3135c522..832fcaba285 100644 --- a/demo/site/src/util/graphQLClient.ts +++ b/demo/site/src/util/graphQLClient.ts @@ -3,7 +3,7 @@ import { createFetchWithDefaults, createGraphQLFetch as createGraphQLFetchLibrary, type SitePreviewData, -} from "@comet/cms-site"; +} from "@comet/site-nextjs"; import { getVisibilityParam } from "./ServerContext"; diff --git a/demo/site/src/util/graphQLClientMiddleware.ts b/demo/site/src/util/graphQLClientMiddleware.ts index b3b09cf6b93..9077a1399e2 100644 --- a/demo/site/src/util/graphQLClientMiddleware.ts +++ b/demo/site/src/util/graphQLClientMiddleware.ts @@ -1,4 +1,4 @@ -import { createFetchWithDefaults, createGraphQLFetch } from "@comet/cms-site"; +import { createFetchWithDefaults, createGraphQLFetch } from "@comet/site-nextjs"; export function createGraphQLFetchMiddleware() { if (!process.env.API_BASIC_AUTH_SYSTEM_USER_PASSWORD) { diff --git a/demo/site/src/util/recursivelyLoadBlockData.ts b/demo/site/src/util/recursivelyLoadBlockData.ts index 0d159b0e291..f854d428de0 100644 --- a/demo/site/src/util/recursivelyLoadBlockData.ts +++ b/demo/site/src/util/recursivelyLoadBlockData.ts @@ -1,8 +1,8 @@ -import { type BlockLoader, type BlockLoaderDependencies, recursivelyLoadBlockData as cometRecursivelyLoadBlockData } from "@comet/cms-site"; +import { type BlockLoader, type BlockLoaderDependencies, recursivelyLoadBlockData as cometRecursivelyLoadBlockData } from "@comet/site-nextjs"; import { loader as newsDetailLoader } from "@src/news/blocks/NewsDetailBlock.loader"; import { loader as newsListLoader } from "@src/news/blocks/NewsListBlock.loader"; -declare module "@comet/cms-site" { +declare module "@comet/site-nextjs" { export interface BlockLoaderDependencies { pageTreeNodeId?: string; } @@ -13,7 +13,7 @@ const blockLoaders: Record = { NewsList: newsListLoader, }; -//small wrapper for @comet/cms-site recursivelyLoadBlockData that injects blockMeta from block-meta.json +//small wrapper for @comet/site-nextjs recursivelyLoadBlockData that injects blockMeta from block-meta.json export async function recursivelyLoadBlockData(options: { blockType: string; blockData: unknown } & BlockLoaderDependencies) { const blocksMeta = await import("../../block-meta.json"); //dynamic import to avoid this json in client bundle return cometRecursivelyLoadBlockData({ ...options, blocksMeta: blocksMeta.default, loaders: blockLoaders }); diff --git a/demo/site/src/util/siteConfig.ts b/demo/site/src/util/siteConfig.ts index a96282400e2..3c02f7e9802 100644 --- a/demo/site/src/util/siteConfig.ts +++ b/demo/site/src/util/siteConfig.ts @@ -1,4 +1,4 @@ -import { previewParams } from "@comet/cms-site"; +import { previewParams } from "@comet/site-nextjs"; import type { PublicSiteConfig } from "@src/site-configs"; import { headers } from "next/headers"; diff --git a/dev-pm.config.js b/dev-pm.config.js index 4ba760b6987..26fce1c54d3 100644 --- a/dev-pm.config.js +++ b/dev-pm.config.js @@ -7,6 +7,8 @@ const packageFolderMapping = { "@comet/cms-admin": "packages/admin/cms-admin", "@comet/cms-api": "packages/api/cms-api", "@comet/cms-site": "packages/site/cms-site", + "@comet/site-nextjs": "packages/site/site-nextjs", + "@comet/site-react": "packages/site/site-react", }; const waitOnPackages = (...packages) => { @@ -109,6 +111,32 @@ module.exports = { group: ["cms-site", "cms"], }, + //group site-nextjs + { + name: "site-nextjs", + script: "pnpm --filter @comet/site-nextjs run dev", + group: ["site-nextjs", "cms"], + waitOn: [...waitOnPackages("@comet/site-react")], + }, + { + name: "site-nextjs-codegen-block-types", + script: "pnpm --filter @comet/site-nextjs run generate-block-types:watch", + group: ["site-nextjs", "cms"], + waitOn: [...waitOnPackages("@comet/site-react")], + }, + + //group site-react + { + name: "site-react", + script: "pnpm --filter @comet/site-react run dev", + group: ["site-react", "site-nextjs", "cms"], + }, + { + name: "site-react-codegen-block-types", + script: "pnpm --filter @comet/site-react run generate-block-types:watch", + group: ["site-react", "site-nextjs", "cms"], + }, + //group demo admin { name: "demo-admin", @@ -153,7 +181,7 @@ module.exports = { name: "demo-site", script: "pnpm --filter comet-demo-site run dev", group: ["demo-site", "demo"], - waitOn: [...waitOnPackages("@comet/cms-site"), "tcp:$API_PORT"], + waitOn: [...waitOnPackages("@comet/site-nextjs"), "tcp:$API_PORT"], }, { name: "demo-site-codegen", diff --git a/docker-compose.yml b/docker-compose.yml index 24d24d01604..6be72703827 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: volumes: - postgres:/bitnami/postgresql ports: - - "${POSTGRESQL_PORT}:5432" + - "127.0.0.1:${POSTGRESQL_PORT}:5432" environment: POSTGRESQL_USER: postgres POSTGRESQL_PASSWORD: ${POSTGRESQL_PWD_DECODED} @@ -18,7 +18,7 @@ services: volumes: - ./demo/uploads:/uploads:ro ports: - - ${IMGPROXY_PORT}:8080 + - "127.0.0.1:${IMGPROXY_PORT}:8080" environment: IMGPROXY_KEY: ${IMGPROXY_KEY} IMGPROXY_SALT: ${IMGPROXY_SALT} @@ -36,8 +36,8 @@ services: jaeger: image: jaegertracing/all-in-one:1 ports: - - ${JAEGER_UI_PORT}:16686 - - ${JAEGER_OLTP_PORT}:4318 #OLTP over HTTP + - "127.0.0.1:${JAEGER_UI_PORT}:16686" + - "127.0.0.1:${JAEGER_OLTP_PORT}:4318" #OLTP over HTTP environment: COLLECTOR_OTLP_ENABLED: "true" COLLECTOR_OTLP_HTTP_HOST_PORT: 0.0.0.0:4318 @@ -46,7 +46,7 @@ services: image: redis:7 command: redis-server --maxmemory 256M --maxmemory-policy allkeys-lru --loglevel warning --requirepass ${REDIS_PASSWORD} ports: - - ${REDIS_PORT}:6379 + - "127.0.0.1:${REDIS_PORT}:6379" networks: - redis diff --git a/docs/docs/3-features-modules/9-brevo-module/features/contacts-without-doi.md b/docs/docs/3-features-modules/9-brevo-module/features/contacts-without-doi.md new file mode 100644 index 00000000000..9e71b519eca --- /dev/null +++ b/docs/docs/3-features-modules/9-brevo-module/features/contacts-without-doi.md @@ -0,0 +1,140 @@ +--- +title: Add contacts without sending double opt-in +--- + +Contacts added to a newsletter are legally obliged to give their consent (usually via double opt-in). There may be scenarios, in which a contact already gave their permission. In this case, this feature allows adding or importing contacts without sending a double opt-in message. + +:::caution +Make sure that your project uses Brevo Module v3.1.0 or later. +::: + +## Allow adding contacts without sending a double opt-in mail + +1. Add `contactsWithoutDoi` to your `AppModule`: + +```diff + BrevoModule.register({ + brevo: { + //... + BlacklistedContacts + } ++ contactsWithoutDoi: { ++ allowAddingContactsWithoutDoi: true, ++ }, + //... + }); +``` + +2. Add `allowAddingContactsWithoutDoi` to the `config.ts` in the api: + +```diff + //... + ecgRtrList: { + apiKey: envVars.ECG_RTR_LIST_API_KEY, + }, ++ contactsWithoutDoi: { ++ allowAddingContactsWithoutDoi: true, ++ }, + //... +``` + +3. Add it to the `config.ts` in the admin: + +```diff + //... + return { + ...cometConfig, + apiUrl: environmentVariables.API_URL, + adminUrl: environmentVariables.ADMIN_URL, + sitesConfig: JSON.parse(environmentVariables.SITES_CONFIG) as SitesConfig, + buildDate: environmentVariables.BUILD_DATE, + buildNumber: environmentVariables.BUILD_NUMBER, + commitSha: environmentVariables.COMMIT_SHA, + campaignUrl: environmentVariables.CAMPAIGN_URL, ++ allowAddingContactsWithoutDoi: true, + } +``` + +4. Add `allowAddingContactsWithoutDoi` to the `BrevoConfigProvider` in your `App.tsx`: + +```diff + //... + { + return `${config.campaignUrl}/block-preview/${scope.domain}/${scope.language}`; + }, ++ allowAddingContactsWithoutDoi: config.allowAddingContactsWithoutDoi, + }} + > +``` + +## Add `BlacklistedContacts` table + +To prevent re-adding contacts, that unsubscribed (are blacklisted), those contacts are hashed and stored in a separate table. Adding a contact again, is only possible, if the contact gives his consent via double opt-in. + +1. Use `createBlacklistedContactsEntity` for creating a `BlacklistedContacts` entity. Pass `Scope` and add it to the `AppModule`: + +```diff + BrevoModule.register({ + brevo: { + //... ++ BlacklistedContacts + } + //... + }); +``` + +2. Add `emailHashKey` to your environment variables: + +```diff ++ @IsString() ++ @Length(64) ++ EMAIL_HASH_KEY: string; +``` + +3. Also add it to the `config.ts` and your `AppModule`: + +```diff + //... + ecgRtrList: { + apiKey: envVars.ECG_RTR_LIST_API_KEY, + }, + contactsWithoutDoi: { + allowAddingContactsWithoutDoi: config.contactsWithoutDoi.allowAddingContactsWithoutDoi, ++ emailHashKey: config.contactsWithoutDoi.emailHashKey, + }, + sitePreviewSecret: envVars.SITE_PREVIEW_SECRET, +``` + +```diff + BrevoModule.register({ + brevo: { + //... + BlacklistedContacts + } + contactsWithoutDoi: { + allowAddingContactsWithoutDoi: config.contactsWithoutDoi.allowAddingContactsWithoutDoi, ++ emailHashKey: config.contactsWithoutDoi.emailHashKey, + }, + //... + }); +``` + +## Add action logging for adding contacts without sending a double opt-in + +When a user adds a contact and skips sending the double opt-in email, the action is logged. + +1. Use `createBrevoEmailImportLogEntity` for creating `BrevoEmailImportLog` entity. Pass `Scope` and add it to the `AppModule`: + +```diff + BrevoModule.register({ + brevo: { + //... ++ BrevoEmailImportLog + } + //... + }); +``` diff --git a/docs/docs/3-features-modules/9-brevo-module/features/index.md b/docs/docs/3-features-modules/9-brevo-module/features/index.md new file mode 100644 index 00000000000..0089b8a39fb --- /dev/null +++ b/docs/docs/3-features-modules/9-brevo-module/features/index.md @@ -0,0 +1,3 @@ +--- +title: Brevo Features +--- diff --git a/install.sh b/install.sh index 70b7b06b0d3..57a9026be25 100755 --- a/install.sh +++ b/install.sh @@ -18,6 +18,8 @@ ln -sf ../../api/cms-api/block-meta.json ./packages/admin/cms-admin/block-meta.j # site CMS ln -sf ../../api/cms-api/block-meta.json ./packages/site/cms-site/block-meta.json +ln -sf ../../api/cms-api/block-meta.json ./packages/site/site-nextjs/block-meta.json +ln -sf ../../api/cms-api/block-meta.json ./packages/site/site-react/block-meta.json # api DEMO ln -sf ../../.env ./demo/api/.env diff --git a/knip.json b/knip.json index dd803c3e0b5..eca0fd6a976 100644 --- a/knip.json +++ b/knip.json @@ -76,6 +76,16 @@ "project": ["./src/**/*.{ts,tsx}"], "ignore": ["./src/**/*.generated.ts"] }, + "packages/site/site-react": { + "entry": ["./src/index.ts"], + "project": ["./src/**/*.{ts,tsx}"], + "ignore": ["./src/**/*.generated.ts"] + }, + "packages/site/site-nextjs": { + "entry": ["./src/index.ts"], + "project": ["./src/**/*.{ts,tsx}"], + "ignore": ["./src/**/*.generated.ts"] + }, "demo/admin": { "entry": ["./src/loader.ts", "./src/**/*.cometGen.{ts,tsx}"], "project": ["./src/**/*.{ts,tsx}"], diff --git a/package.json b/package.json index 48ded8f6dfe..3c0a162406b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "dev:cms:admin": "dev-pm start @cms-admin", "dev:cms:api": "dev-pm start @cms-api", "dev:cms:site": "dev-pm start @cms-site", + "dev:cms:site-nextjs": "dev-pm start @site-nextjs", + "dev:cms:site-react": "dev-pm start @site-react", "dev:demo": "dev-pm start @demo", "dev:demo:admin": "dev-pm start @demo-admin", "dev:demo:api": "dev-pm start @demo-api", diff --git a/packages/admin/admin-babel-preset/CHANGELOG.md b/packages/admin/admin-babel-preset/CHANGELOG.md index bd3020bd53f..3927d2d9dc4 100644 --- a/packages/admin/admin-babel-preset/CHANGELOG.md +++ b/packages/admin/admin-babel-preset/CHANGELOG.md @@ -21,6 +21,12 @@ - 682a674: Add support for React 18 +## 7.21.1 + +## 7.21.0 + +## 7.20.0 + ## 7.19.0 ## 7.18.0 diff --git a/packages/admin/admin-color-picker/CHANGELOG.md b/packages/admin/admin-color-picker/CHANGELOG.md index a49a852b33b..a0279cbd02f 100644 --- a/packages/admin/admin-color-picker/CHANGELOG.md +++ b/packages/admin/admin-color-picker/CHANGELOG.md @@ -87,6 +87,33 @@ - @comet/admin@8.0.0-beta.0 - @comet/admin-icons@8.0.0-beta.0 +## 7.21.1 + +### Patch Changes + +- Updated dependencies [b771bd6d8] + - @comet/admin@7.21.1 + - @comet/admin-icons@7.21.1 + +## 7.21.0 + +### Patch Changes + +- Updated dependencies [1a30eb858] +- Updated dependencies [3e9ea613e] + - @comet/admin@7.21.0 + - @comet/admin-icons@7.21.0 + +## 7.20.0 + +### Patch Changes + +- Updated dependencies [415a83165] +- Updated dependencies [99f904f81] +- Updated dependencies [2d1726543] + - @comet/admin@7.20.0 + - @comet/admin-icons@7.20.0 + ## 7.19.0 ### Patch Changes diff --git a/packages/admin/admin-date-time/CHANGELOG.md b/packages/admin/admin-date-time/CHANGELOG.md index cecc7da4b73..77d1698aa97 100644 --- a/packages/admin/admin-date-time/CHANGELOG.md +++ b/packages/admin/admin-date-time/CHANGELOG.md @@ -87,6 +87,34 @@ - @comet/admin@8.0.0-beta.0 - @comet/admin-icons@8.0.0-beta.0 +## 7.21.1 + +### Patch Changes + +- Updated dependencies [b771bd6d8] + - @comet/admin@7.21.1 + - @comet/admin-icons@7.21.1 + +## 7.21.0 + +### Patch Changes + +- 1a30eb858: Adapt styling of `DateTimePicker` on mobile devices to improve readability of the placeholder +- Updated dependencies [1a30eb858] +- Updated dependencies [3e9ea613e] + - @comet/admin@7.21.0 + - @comet/admin-icons@7.21.0 + +## 7.20.0 + +### Patch Changes + +- Updated dependencies [415a83165] +- Updated dependencies [99f904f81] +- Updated dependencies [2d1726543] + - @comet/admin@7.20.0 + - @comet/admin-icons@7.20.0 + ## 7.19.0 ### Patch Changes diff --git a/packages/admin/admin-date-time/src/dateTimePicker/DateTimePicker.tsx b/packages/admin/admin-date-time/src/dateTimePicker/DateTimePicker.tsx index 99df0d1b5c9..30e34f18965 100644 --- a/packages/admin/admin-date-time/src/dateTimePicker/DateTimePicker.tsx +++ b/packages/admin/admin-date-time/src/dateTimePicker/DateTimePicker.tsx @@ -14,37 +14,67 @@ export type DateTimePickerClassKey = "root" | "dateFormControl" | "timeFormContr const Root = createComponentSlot("div")({ componentName: "DateTimePicker", slotName: "root", -})(css` - display: flex; - align-items: center; -`); +})( + ({ theme }) => css` + ${theme.breakpoints.up("sm")} { + display: flex; + align-items: center; + } + `, +); const DateFormControl = createComponentSlot(FormControl)({ componentName: "DateTimePicker", slotName: "dateFormControl", })( ({ theme }) => css` - flex-grow: 1; - margin-right: ${theme.spacing(2)}; + margin-bottom: ${theme.spacing(2)}; + width: 100%; + + ${theme.breakpoints.up("sm")} { + flex-grow: 1; + margin-right: ${theme.spacing(2)}; + margin-bottom: 0; + width: auto; + } `, ); const TimeFormControl = createComponentSlot(FormControl)({ componentName: "DateTimePicker", slotName: "timeFormControl", -})(css` - flex-grow: 1; -`); +})( + ({ theme }) => css` + width: 100%; + flex-grow: 1; + + ${theme.breakpoints.up("sm")} { + width: auto; + } + `, +); const DatePicker = createComponentSlot(DatePickerBase)({ componentName: "DateTimePicker", slotName: "datePicker", -})(); +})( + () => css` + .MuiInputBase-input { + text-overflow: ellipsis; + } + `, +); const TimePicker = createComponentSlot(TimePickerBase)({ componentName: "DateTimePicker", slotName: "timePicker", -})(); +})( + () => css` + .MuiInputBase-input { + text-overflow: ellipsis; + } + `, +); export interface DateTimePickerProps extends ThemedComponentBaseProps<{ diff --git a/packages/admin/admin-icons/CHANGELOG.md b/packages/admin/admin-icons/CHANGELOG.md index 061810f9a5f..7df8acd3a58 100644 --- a/packages/admin/admin-icons/CHANGELOG.md +++ b/packages/admin/admin-icons/CHANGELOG.md @@ -36,6 +36,12 @@ - 682a674: Add support for React 18 +## 7.21.1 + +## 7.21.0 + +## 7.20.0 + ## 7.19.0 ## 7.18.0 diff --git a/packages/admin/admin-rte/CHANGELOG.md b/packages/admin/admin-rte/CHANGELOG.md index 0a2a80fcdd0..d4c781271da 100644 --- a/packages/admin/admin-rte/CHANGELOG.md +++ b/packages/admin/admin-rte/CHANGELOG.md @@ -86,6 +86,33 @@ - @comet/admin@8.0.0-beta.0 - @comet/admin-icons@8.0.0-beta.0 +## 7.21.1 + +### Patch Changes + +- Updated dependencies [b771bd6d8] + - @comet/admin@7.21.1 + - @comet/admin-icons@7.21.1 + +## 7.21.0 + +### Patch Changes + +- Updated dependencies [1a30eb858] +- Updated dependencies [3e9ea613e] + - @comet/admin@7.21.0 + - @comet/admin-icons@7.21.0 + +## 7.20.0 + +### Patch Changes + +- Updated dependencies [415a83165] +- Updated dependencies [99f904f81] +- Updated dependencies [2d1726543] + - @comet/admin@7.20.0 + - @comet/admin-icons@7.20.0 + ## 7.19.0 ### Patch Changes diff --git a/packages/admin/admin/CHANGELOG.md b/packages/admin/admin/CHANGELOG.md index 8ccf0782213..ba2cb0e6357 100644 --- a/packages/admin/admin/CHANGELOG.md +++ b/packages/admin/admin/CHANGELOG.md @@ -353,6 +353,36 @@ - Updated dependencies [682a674] - @comet/admin-icons@8.0.0-beta.0 +## 7.21.1 + +### Patch Changes + +- b771bd6d8: Don't delete an item when closing the delete dialog in `CrudContextMenu` + - @comet/admin-icons@7.21.1 + - @comet/admin-theme@7.21.1 + +## 7.21.0 + +### Patch Changes + +- 1a30eb858: Prevent overlapping placeholders by a non-visible clear-button +- 3e9ea613e: Fix color of button in `UndoSnackbar` + - @comet/admin-icons@7.21.0 + - @comet/admin-theme@7.21.0 + +## 7.20.0 + +### Patch Changes + +- 415a83165: Prevent form components used within `Field`/`FieldContainer` from overflowing their parent + + Select components now truncate their value with ellipsis when used within these components, consistent with their behavior in other usages. + +- 99f904f81: Close `Dialog` with ESC key or backdrop click +- 2d1726543: `title` prop of the Dialog got merged with `title` Prop of `MuiDialogProps`. This lead to errors when forwarding ReactNodes to title. + - @comet/admin-icons@7.20.0 + - @comet/admin-theme@7.20.0 + ## 7.19.0 ### Patch Changes diff --git a/packages/admin/admin/src/common/ClearInputAdornment.tsx b/packages/admin/admin/src/common/ClearInputAdornment.tsx index 6c3784383b1..63fba7ea734 100644 --- a/packages/admin/admin/src/common/ClearInputAdornment.tsx +++ b/packages/admin/admin/src/common/ClearInputAdornment.tsx @@ -45,6 +45,10 @@ export const ClearInputAdornment = (inProps: ClearInputAdornmentProps) => { position, }; + if (!hasClearableContent) { + return null; + } + return ( diff --git a/packages/admin/admin/src/common/DeleteDialog.tsx b/packages/admin/admin/src/common/DeleteDialog.tsx index 67c8c34213b..188b3a7c5a0 100644 --- a/packages/admin/admin/src/common/DeleteDialog.tsx +++ b/packages/admin/admin/src/common/DeleteDialog.tsx @@ -22,7 +22,7 @@ export const DeleteDialog = (props: DeleteDialogProps) => { const { dialogOpen, onDelete, onCancel } = props; return ( - + diff --git a/packages/admin/admin/src/common/Dialog.tsx b/packages/admin/admin/src/common/Dialog.tsx index 0110786d1ff..3d2dd23bb26 100644 --- a/packages/admin/admin/src/common/Dialog.tsx +++ b/packages/admin/admin/src/common/Dialog.tsx @@ -26,7 +26,7 @@ export type DialogProps = ThemedComponentBaseProps<{ iconMapping?: { closeIcon?: ReactNode; }; -} & MuiDialogProps; +} & Omit; type OwnerState = { hasCloseButton: boolean; @@ -49,7 +49,7 @@ export function Dialog(inProps: DialogProps) { }; return ( - + {onClose && ( onClose(event, "escapeKeyDown")}> {closeIcon} diff --git a/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx b/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx index ba3f8ee8d38..0ebecc8ded2 100644 --- a/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx +++ b/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx @@ -141,7 +141,7 @@ export function CrudMoreActionsMenu({ slotProps, overallActions, selectiveAction return ( - } {...buttonProps} onClick={handleClick}> + } {...buttonProps} onClick={handleClick} responsive> {!!selectionSize && } diff --git a/packages/admin/admin/src/form/FieldContainer.tsx b/packages/admin/admin/src/form/FieldContainer.tsx index 0e26da2fa4d..fe8f329098f 100644 --- a/packages/admin/admin/src/form/FieldContainer.tsx +++ b/packages/admin/admin/src/form/FieldContainer.tsx @@ -200,6 +200,8 @@ const InputContainer = createComponentSlot("div") css` + overflow: hidden; + ${ownerState.variant === "horizontal" && ownerState.fullWidth && css` diff --git a/packages/admin/admin/src/snackbar/UndoSnackbar.tsx b/packages/admin/admin/src/snackbar/UndoSnackbar.tsx index b745984987c..c02eaf887a7 100644 --- a/packages/admin/admin/src/snackbar/UndoSnackbar.tsx +++ b/packages/admin/admin/src/snackbar/UndoSnackbar.tsx @@ -26,7 +26,7 @@ export const UndoSnackbar = ({ onUndoClick, payload, ...props }: UndoS anchorOrigin={{ vertical: "bottom", horizontal: "left" }} autoHideDuration={5000} action={ - diff --git a/packages/admin/cms-admin/src/redirects/RedirectActiveness.tsx b/packages/admin/cms-admin/src/redirects/RedirectActiveness.tsx index 5d9ddada3f9..af649dd54b3 100644 --- a/packages/admin/cms-admin/src/redirects/RedirectActiveness.tsx +++ b/packages/admin/cms-admin/src/redirects/RedirectActiveness.tsx @@ -17,6 +17,7 @@ const updateRedirectActivenessMutation = gql` updateRedirectActiveness(id: $id, input: $input) { id active + activatedAt } } `; @@ -57,6 +58,7 @@ const RedirectActiveness = ({ redirect }: RedirectActivenessProps): JSX.Element __typename: "Redirect", id: redirect.id, active: active, + activatedAt: active ? new Date() : null, }, }, }); diff --git a/packages/admin/cms-admin/src/redirects/RedirectsGrid.gql.ts b/packages/admin/cms-admin/src/redirects/RedirectsGrid.gql.ts index c191507a98f..f676dd2d3b3 100644 --- a/packages/admin/cms-admin/src/redirects/RedirectsGrid.gql.ts +++ b/packages/admin/cms-admin/src/redirects/RedirectsGrid.gql.ts @@ -8,6 +8,7 @@ const redirectTableFragment = gql` fragment RedirectTable on Redirect { id active + activatedAt sourceType source target diff --git a/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx b/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx index 46861a3a10d..d04d868ce29 100644 --- a/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx +++ b/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx @@ -98,7 +98,7 @@ export function RedirectsGrid({ linkBlock, scope }: Props): JSX.Element { field: "generationType", headerName: intl.formatMessage({ id: "comet.pages.redirects.redirect.generationType", - defaultMessage: "GenerationType", + defaultMessage: "Generation Type", }), renderCell: (params) => ( @@ -113,6 +113,7 @@ export function RedirectsGrid({ linkBlock, scope }: Props): JSX.Element { filterOperators: getGridSingleSelectOperators(), type: "singleSelect", valueOptions: typeOptions, + width: 130, }, { field: "active", @@ -124,6 +125,17 @@ export function RedirectsGrid({ linkBlock, scope }: Props): JSX.Element { sortable: false, type: "boolean", }, + { + field: "activatedAt", + headerName: intl.formatMessage({ + id: "comet.pages.redirects.redirect.activatedAt", + defaultMessage: "Activation Date", + }), + sortable: false, + type: "dateTime", + valueGetter: ({ value }) => value && new Date(value), + width: 170, + }, { field: "actions", type: "actions", diff --git a/packages/api/cms-api/CHANGELOG.md b/packages/api/cms-api/CHANGELOG.md index 64120631e19..b9fba21848e 100644 --- a/packages/api/cms-api/CHANGELOG.md +++ b/packages/api/cms-api/CHANGELOG.md @@ -158,6 +158,45 @@ - 7e7a4aa: Fix `title` field not added to types in `createLinkBlock` - f20ec6c: Make class-validator a peer dependency +## 7.21.1 + +### Patch Changes + +- @comet/blocks-api@7.21.1 + +## 7.21.0 + +### Patch Changes + +- 06920eb59: Fix: Change GraphQL Type of numberOfDescendants from Float to Int + - @comet/blocks-api@7.21.0 + +## 7.20.0 + +### Minor Changes + +- ea26f5d89: Add a nullable column `activatedAt` to `Redirects` table to display the latest activation date of a redirect + +### Patch Changes + +- 557e311ea: AccessLog: Remove some DAM URLs from log + + Hashed URLs and preview URLs are not useful in the logs, so we remove them. + +- 21f95adfe: DAM: Fix headers + + While we fixed a few issues with cache control headers in https://github.com/vivid-planet/comet/pull/2653, there are still a few issues which need to be addressed. The following changes are part of a series of changes which will address the issues: + + - Only store the `content-type` header + - Prevent imgproxy headers from being passed through to the client + - Remove redundantly stored `content-type` for Azure storage accounts and S3 buckets + +- f3b5b57b7: DAM: Set `cache-control: no-store` for folder download + + Explicitly set `cache-control: no-store` for folder download to prevent caching of the response. Normally this should not be cached by any CDN, because the Request contains a cookie, but it is better to be explicit about it. + + - @comet/blocks-api@7.20.0 + ## 7.19.0 ### Minor Changes diff --git a/packages/api/cms-api/schema.gql b/packages/api/cms-api/schema.gql index 9e15484f82e..9b3070160ae 100644 --- a/packages/api/cms-api/schema.gql +++ b/packages/api/cms-api/schema.gql @@ -220,7 +220,7 @@ type PageTreeNode { updatedAt: DateTime! category: String! childNodes: [PageTreeNode!]! - numberOfDescendants: Float! + numberOfDescendants: Int! parentNode: PageTreeNode path: String! parentNodes: [PageTreeNode!]! @@ -259,6 +259,7 @@ type Redirect { target: JSONObject! comment: String active: Boolean! + activatedAt: DateTime generationType: RedirectGenerationType! createdAt: DateTime! updatedAt: DateTime! diff --git a/packages/api/cms-api/src/access-log/access-log.interceptor.ts b/packages/api/cms-api/src/access-log/access-log.interceptor.ts index 4ce37e4c1b7..a597fb250fc 100644 --- a/packages/api/cms-api/src/access-log/access-log.interceptor.ts +++ b/packages/api/cms-api/src/access-log/access-log.interceptor.ts @@ -1,5 +1,6 @@ import { CallHandler, ExecutionContext, Inject, Injectable, Logger, NestInterceptor, Optional } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; +import { Request } from "express"; import { GraphQLResolveInfo } from "graphql"; import { getClientIp } from "request-ip"; @@ -8,7 +9,7 @@ import { User } from "../user-permissions/interfaces/user"; import { ACCESS_LOG_CONFIG } from "./access-log.constants"; import { AccessLogConfig } from "./access-log.module"; -const IGNORED_PATHS = ["/dam/images/:hash/:fileId", "/dam/files/:hash/:fileId", "/dam/images/preview/:fileId", "/dam/files/preview/:fileId"]; +const IGNORED_ROUTES = ["/dam/images/", "/dam/files/preview", "/dam/files/download", "/dam/files/:hash/"]; @Injectable() export class AccessLogInterceptor implements NestInterceptor { @@ -59,14 +60,16 @@ export class AccessLogInterceptor implements NestInterceptor { requestData.push(`args: ${JSON.stringify(gqlArgs)}`); } else { const httpContext = context.switchToHttp(); - const httpRequest = httpContext.getRequest(); + const httpRequest = httpContext.getRequest(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const user = (httpRequest as any).user as CurrentUser; if ( - IGNORED_PATHS.some((ignoredPath) => httpRequest.route.path.includes(ignoredPath)) || + IGNORED_ROUTES.some((ignoredPath) => httpRequest.route.path.includes(ignoredPath)) || (this.config && this.config.shouldLogRequest && !this.config.shouldLogRequest({ - user: httpRequest.user, + user: user, req: httpRequest, })) ) { @@ -75,7 +78,7 @@ export class AccessLogInterceptor implements NestInterceptor { const ipAddress = getClientIp(httpRequest); requestData.push(`ip: ${ipAddress}`); - this.pushUserToRequestData(httpRequest.user, requestData); + this.pushUserToRequestData(user, requestData); requestData.push( ...[`method: ${httpRequest.method}`, `route: ${httpRequest.route.path}`, `params: ${JSON.stringify(httpRequest.params)}`], diff --git a/packages/api/cms-api/src/blob-storage/backends/azure/blob-storage-azure.storage.ts b/packages/api/cms-api/src/blob-storage/backends/azure/blob-storage-azure.storage.ts index 060125fc461..e10dda66a08 100644 --- a/packages/api/cms-api/src/blob-storage/backends/azure/blob-storage-azure.storage.ts +++ b/packages/api/cms-api/src/blob-storage/backends/azure/blob-storage-azure.storage.ts @@ -1,13 +1,15 @@ import { type BlobHTTPHeaders, BlobServiceClient, RestError, StorageSharedKeyCredential } from "@azure/storage-blob"; +import { Logger } from "@nestjs/common"; import { Readable } from "stream"; import { type BlobStorageBackendInterface, type CreateFileOptions, type StorageMetaData } from "../blob-storage-backend.interface"; import { type BlobStorageAzureConfig } from "./blob-storage-azure.config"; export class BlobStorageAzureStorage implements BlobStorageBackendInterface { + private readonly logger = new Logger(BlobStorageAzureStorage.name); private readonly client: BlobServiceClient; - constructor(private readonly config: BlobStorageAzureConfig["azure"]) { + constructor(readonly config: BlobStorageAzureConfig["azure"]) { const sharedKeyCredential = new StorageSharedKeyCredential(config.accountName, config.accountKey); this.client = new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, sharedKeyCredential); } @@ -29,10 +31,10 @@ export class BlobStorageAzureStorage implements BlobStorageBackendInterface { // retry the creation for three times if the container is being deleted, waiting 30 seconds between each attempt. // See https://docs.microsoft.com/en-us/rest/api/storageservices/delete-container#remarks for more information. if (error instanceof RestError && error.code === "ContainerBeingDeleted") { - console.info(`Container is being deleted, retrying in 30s`); + this.logger.log(`Container (${folderName}) is being deleted, retrying in 30s`); await this.sleep(30); currentAttempt++; - console.info(`Retrying... (Attempt ${currentAttempt} of ${maxNumberOfAttempts})`); + this.logger.log(`Retrying to create container (${folderName})... (Attempt ${currentAttempt} of ${maxNumberOfAttempts})`); continue; } @@ -53,23 +55,19 @@ export class BlobStorageAzureStorage implements BlobStorageBackendInterface { folderName: string, fileName: string, data: NodeJS.ReadableStream | Buffer | string, - { headers }: CreateFileOptions, + { contentType }: CreateFileOptions, ): Promise { - const metadata = { - headers: JSON.stringify(headers), - }; const blobHTTPHeaders: BlobHTTPHeaders = { - blobContentType: headers["content-type"], + blobContentType: contentType, }; const blockBlobClient = this.client.getContainerClient(folderName).getBlockBlobClient(fileName); if (typeof data === "string") { - await blockBlobClient.uploadFile(data, { metadata, blobHTTPHeaders }); + await blockBlobClient.uploadFile(data, { blobHTTPHeaders }); } else if (Buffer.isBuffer(data)) { - await blockBlobClient.uploadData(data, { metadata, blobHTTPHeaders }); + await blockBlobClient.uploadData(data, { blobHTTPHeaders }); } else { await blockBlobClient.uploadStream(new Readable().wrap(data), undefined, undefined, { - metadata, blobHTTPHeaders, }); } @@ -99,7 +97,8 @@ export class BlobStorageAzureStorage implements BlobStorageBackendInterface { size: properties.contentLength!, // is defined in node.js but not for browsers etag: properties.etag, lastModified: properties.lastModified, - headers: properties.metadata?.headers ? JSON.parse(properties.metadata.headers) : {}, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + contentType: properties.contentType!, }; } diff --git a/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.interface.ts b/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.interface.ts index 24babd7a7d1..cb9d827507e 100644 --- a/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.interface.ts +++ b/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.interface.ts @@ -2,14 +2,14 @@ import { type Readable } from "stream"; export type StorageMetaData = { size: number; - etag?: string; - lastModified?: Date; - headers: Record; + etag?: string; // TODO: currently unused, consider removing + lastModified?: Date; // TODO: currently unused, consider removing + contentType: string; }; export type CreateFileOptions = { size: number; - headers: StorageMetaData["headers"]; + contentType: string; }; export interface BlobStorageBackendInterface { diff --git a/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.service.ts b/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.service.ts index 857234c132f..2ab702d4edc 100644 --- a/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.service.ts +++ b/packages/api/cms-api/src/blob-storage/backends/blob-storage-backend.service.ts @@ -13,7 +13,8 @@ import { BlobStorageS3Storage } from "./s3/blob-storage-s3.storage"; @Injectable() export class BlobStorageBackendService implements BlobStorageBackendInterface { private readonly backend: BlobStorageBackendInterface; - constructor(@Inject(BLOB_STORAGE_CONFIG) private readonly config: BlobStorageConfig) { + + constructor(@Inject(BLOB_STORAGE_CONFIG) readonly config: BlobStorageConfig) { if (config.backend.driver === "file") { this.backend = new BlobStorageFileStorage(config.backend.file); } else if (config.backend.driver === "azure") { @@ -43,9 +44,9 @@ export class BlobStorageBackendService implements BlobStorageBackendInterface { folderName: string, fileName: string, data: NodeJS.ReadableStream | Buffer | string, - { headers, ...options }: CreateFileOptions, + { contentType, ...options }: CreateFileOptions, ): Promise { - return this.backend.createFile(folderName, fileName, data, { ...options, headers: normalizeHeaders(headers) }); + return this.backend.createFile(folderName, fileName, data, { ...options, contentType: contentType }); } async getFile(folderName: string, fileName: string): Promise { @@ -61,8 +62,7 @@ export class BlobStorageBackendService implements BlobStorageBackendInterface { } async getFileMetaData(folderName: string, fileName: string): Promise { - const metaData = await this.backend.getFileMetaData(folderName, fileName); - return { ...metaData, headers: normalizeHeaders(metaData.headers) }; + return this.backend.getFileMetaData(folderName, fileName); } getBackendFilePathPrefix(): string { @@ -79,20 +79,8 @@ export class BlobStorageBackendService implements BlobStorageBackendInterface { if (!(await this.fileExists(folderName, objectName))) { await this.createFile(folderName, objectName, file.path, { size: file.size, - headers: { - "Content-Type": file.mimetype, - }, + contentType: file.mimetype, }); } } } - -const normalizeHeaders = (headers: CreateFileOptions["headers"]): CreateFileOptions["headers"] => { - const result: CreateFileOptions["headers"] = {}; - - for (const [key, value] of Object.entries(headers)) { - result[key.toLowerCase()] = value; - } - - return result; -}; diff --git a/packages/api/cms-api/src/blob-storage/backends/file/blob-storage-file.storage.ts b/packages/api/cms-api/src/blob-storage/backends/file/blob-storage-file.storage.ts index f7f2d72c631..549d90ca34d 100644 --- a/packages/api/cms-api/src/blob-storage/backends/file/blob-storage-file.storage.ts +++ b/packages/api/cms-api/src/blob-storage/backends/file/blob-storage-file.storage.ts @@ -49,7 +49,7 @@ export class BlobStorageFileStorage implements BlobStorageBackendInterface { folderName: string, fileName: string, data: NodeJS.ReadableStream | Buffer | string, - { headers }: CreateFileOptions, + { contentType }: CreateFileOptions, ): Promise { if (!(await this.folderExists(`${folderName}/${path.dirname(fileName)}`))) { await this.createFolder(`${folderName}/${path.dirname(fileName)}`); @@ -70,9 +70,13 @@ export class BlobStorageFileStorage implements BlobStorageBackendInterface { stream.end(); } }), - await fs.promises.writeFile(`${this.path}/${folderName}/${fileName}-${this.headersFile}`, JSON.stringify(headers), { - encoding: "utf-8", - }), + await fs.promises.writeFile( + `${this.path}/${folderName}/${fileName}-${this.headersFile}`, + JSON.stringify({ "content-type": contentType }), + { + encoding: "utf-8", + }, + ), ]); } @@ -104,7 +108,7 @@ export class BlobStorageFileStorage implements BlobStorageBackendInterface { return { size: stat.size, lastModified: stat.mtime, - headers, + contentType: headers["content-type"], }; } diff --git a/packages/api/cms-api/src/blob-storage/backends/s3/blob-storage-s3.storage.ts b/packages/api/cms-api/src/blob-storage/backends/s3/blob-storage-s3.storage.ts index 611974c1747..622876e51f9 100644 --- a/packages/api/cms-api/src/blob-storage/backends/s3/blob-storage-s3.storage.ts +++ b/packages/api/cms-api/src/blob-storage/backends/s3/blob-storage-s3.storage.ts @@ -79,17 +79,12 @@ export class BlobStorageS3Storage implements BlobStorageBackendInterface { folderName: string, fileName: string, data: NodeJS.ReadableStream | Buffer | string, - { headers, size }: CreateFileOptions, + { contentType, size }: CreateFileOptions, ): Promise { - const metadata = { - headers: JSON.stringify(headers), - }; - const input: AWS.PutObjectCommandInput = { ...this.getCommandInput(folderName, fileName), - ContentType: headers["content-type"], + ContentType: contentType, ContentLength: size, - Metadata: metadata, }; if (typeof data === "string") { @@ -133,7 +128,8 @@ export class BlobStorageS3Storage implements BlobStorageBackendInterface { size: response.ContentLength!, etag: response.ETag, lastModified: response.LastModified, - headers: response.Metadata?.headers ? JSON.parse(response.Metadata.headers) : {}, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + contentType: response.ContentType!, }; } diff --git a/packages/api/cms-api/src/blob-storage/cache/scaled-images-cache.service.ts b/packages/api/cms-api/src/blob-storage/cache/scaled-images-cache.service.ts index ebcd617c012..1f6f882fc99 100644 --- a/packages/api/cms-api/src/blob-storage/cache/scaled-images-cache.service.ts +++ b/packages/api/cms-api/src/blob-storage/cache/scaled-images-cache.service.ts @@ -39,7 +39,10 @@ export class ScaledImagesCacheService { } const path = [createHashedPath(fileIdentifier), hasha(scaleSettingsCacheKey, { algorithm: "md5" })].join(sep); - await this.blobStorageBackendService.createFile(this.config.cacheDirectory, path, file, { size: metaData.size, headers: metaData.headers }); + await this.blobStorageBackendService.createFile(this.config.cacheDirectory, path, file, { + size: metaData.size, + contentType: metaData.contentType, + }); } async delete(fileIdentifier: string, scaleSettingsCacheKey?: string): Promise { diff --git a/packages/api/cms-api/src/dam/files/folders.controller.ts b/packages/api/cms-api/src/dam/files/folders.controller.ts index c4ebfdd4f22..b58268791f0 100644 --- a/packages/api/cms-api/src/dam/files/folders.controller.ts +++ b/packages/api/cms-api/src/dam/files/folders.controller.ts @@ -31,6 +31,7 @@ export class FoldersController { res.setHeader("Content-Disposition", `attachment; filename="${folder.name}.zip"`); res.setHeader("Content-Type", "application/zip"); + res.setHeader("cache-control", "no-store"); zipStream.pipe(res); } } diff --git a/packages/api/cms-api/src/dam/images/images.controller.ts b/packages/api/cms-api/src/dam/images/images.controller.ts index efcdebb6111..a046e4667c9 100644 --- a/packages/api/cms-api/src/dam/images/images.controller.ts +++ b/packages/api/cms-api/src/dam/images/images.controller.ts @@ -53,7 +53,6 @@ export class ImagesController { } const file = await this.filesService.findOneById(params.fileId); - if (file === null) { throw new NotFoundException(); } @@ -66,7 +65,7 @@ export class ImagesController { throw new ForbiddenException(); } - return this.getCroppedImage(file, params, accept, res, { + return this.pipeCroppedImage(file, params, accept, res, { "cache-control": "max-age=31536000, private", // Local caches only (1 year) }); } @@ -96,7 +95,7 @@ export class ImagesController { throw new ForbiddenException(); } - return this.getCroppedImage(file, params, accept, res, { + return this.pipeCroppedImage(file, params, accept, res, { "cache-control": "max-age=31536000, private", // Local caches only (1 year) }); } @@ -109,7 +108,6 @@ export class ImagesController { } const file = await this.filesService.findOneById(params.fileId); - if (file === null) { throw new NotFoundException(); } @@ -118,7 +116,7 @@ export class ImagesController { throw new BadRequestException("Content Hash mismatch!"); } - return this.getCroppedImage(file, params, accept, res, { + return this.pipeCroppedImage(file, params, accept, res, { "cache-control": "max-age=86400, public", // Public cache (1 day) }); } @@ -131,7 +129,6 @@ export class ImagesController { } const file = await this.filesService.findOneById(params.fileId); - if (file === null) { throw new NotFoundException(); } @@ -140,7 +137,7 @@ export class ImagesController { throw new BadRequestException("Content Hash mismatch!"); } - return this.getCroppedImage(file, params, accept, res, { + return this.pipeCroppedImage(file, params, accept, res, { "cache-control": "max-age=86400, public", // Public cache (1 day) }); } @@ -149,12 +146,12 @@ export class ImagesController { return hash === this.imagesService.createHash(imageParams); } - private async getCroppedImage( + private async pipeCroppedImage( file: FileInterface, { cropArea, resizeWidth, resizeHeight, focalPoint }: ImageParams, accept: string, res: Response, - overrideHeaders?: OutgoingHttpHeaders, + headers?: OutgoingHttpHeaders, ): Promise { if (!file.image) { throw new NotFoundException(); @@ -227,11 +224,17 @@ export class ImagesController { throw new Error("Response body is null"); } - const headers: Record = {}; - for (const [key, value] of response.headers.entries()) { - headers[key] = value; + const contentLength = response.headers.get("content-length"); + if (!contentLength) { + throw new Error("Content length not found"); } - res.writeHead(response.status, { ...headers, ...overrideHeaders }); + + const contentType = response.headers.get("content-type"); + if (!contentType) { + throw new Error("Content type not found"); + } + + res.writeHead(response.status, { ...headers, "content-length": contentLength, "content-type": contentType }); const readableBody = Readable.fromWeb(response.body); readableBody.pipe(new PassThrough()).pipe(res); @@ -240,13 +243,13 @@ export class ImagesController { await this.cacheService.set(file.contentHash, path, { file: readableBody.pipe(new PassThrough()), metaData: { - size: Number(headers["content-length"]), - headers, + size: Number(contentLength), + contentType: contentType, }, }); } } else { - res.writeHead(200, { ...cache.metaData.headers, ...overrideHeaders }); + res.writeHead(200, { ...headers, "content-type": cache.metaData.contentType, "content-length": cache.metaData.size }); cache.file.pipe(res); } diff --git a/packages/api/cms-api/src/file-uploads/file-uploads-download.controller.ts b/packages/api/cms-api/src/file-uploads/file-uploads-download.controller.ts index bbeb1754ebb..bf82d181613 100644 --- a/packages/api/cms-api/src/file-uploads/file-uploads-download.controller.ts +++ b/packages/api/cms-api/src/file-uploads/file-uploads-download.controller.ts @@ -155,11 +155,17 @@ export function createFileUploadsDownloadController(options: { public: boolean } throw new Error("Response body is null"); } - const headers: Record = {}; - for (const [key, value] of response.headers.entries()) { - headers[key] = value; + const contentLength = response.headers.get("content-length"); + if (!contentLength) { + throw new Error("Content length not found"); } - res.writeHead(response.status, headers); + + const contentType = response.headers.get("content-type"); + if (!contentType) { + throw new Error("Content type not found"); + } + + res.writeHead(response.status, { "content-length": contentLength, "content-type": contentType }); const readableBody = Readable.fromWeb(response.body); readableBody.pipe(new PassThrough()).pipe(res); @@ -168,13 +174,13 @@ export function createFileUploadsDownloadController(options: { public: boolean } await this.cacheService.set(file.contentHash, path, { file: readableBody.pipe(new PassThrough()), metaData: { - size: Number(headers["content-length"]), - headers, + size: Number(contentLength), + contentType: contentType, }, }); } } else { - res.writeHead(200, cache.metaData.headers); + res.writeHead(200, { "content-type": cache.metaData.contentType, "content-length": cache.metaData.size }); cache.file.pipe(res); } diff --git a/packages/api/cms-api/src/mikro-orm/migrations/Migration20250403134629.ts b/packages/api/cms-api/src/mikro-orm/migrations/Migration20250403134629.ts new file mode 100644 index 00000000000..c6f1ca14cd6 --- /dev/null +++ b/packages/api/cms-api/src/mikro-orm/migrations/Migration20250403134629.ts @@ -0,0 +1,7 @@ +import { Migration } from "@mikro-orm/migrations"; + +export class Migration20250403134629 extends Migration { + async up(): Promise { + this.addSql('alter table "Redirect" add column "activatedAt" timestamp with time zone null;'); + } +} diff --git a/packages/api/cms-api/src/mikro-orm/mikro-orm.module.ts b/packages/api/cms-api/src/mikro-orm/mikro-orm.module.ts index d0d942f4318..333e3979e10 100644 --- a/packages/api/cms-api/src/mikro-orm/mikro-orm.module.ts +++ b/packages/api/cms-api/src/mikro-orm/mikro-orm.module.ts @@ -28,6 +28,7 @@ import { Migration20240725071750 } from "./migrations/Migration20240725071750"; import { Migration20240814090503 } from "./migrations/Migration20240814090503"; import { Migration20240814090541 } from "./migrations/Migration20240814090541"; import { Migration20240814090653 } from "./migrations/Migration20240814090653"; +import { Migration20250403134629 } from "./migrations/Migration20250403134629"; export interface MikroOrmModuleOptions { ormConfig: MikroOrmNestjsOptions; @@ -91,6 +92,7 @@ export function createOrmConfig({ migrations, ...defaults }: MikroOrmNestjsOptio { name: "Migration20240814090503", class: Migration20240814090503 }, { name: "Migration20240814090541", class: Migration20240814090541 }, { name: "Migration20240814090653", class: Migration20240814090653 }, + { name: "Migration20250403134629", class: Migration20250403134629 }, ...(migrations?.migrationsList || []), ].sort((migrationA, migrationB) => { if (migrationA.name < migrationB.name) { diff --git a/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts b/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts index a4b699c643f..1380f2cd837 100644 --- a/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts +++ b/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts @@ -1,5 +1,5 @@ import { Inject, Type } from "@nestjs/common"; -import { Args, ArgsType, createUnionType, ID, Info, Mutation, ObjectType, Parent, Query, ResolveField, Resolver, Union } from "@nestjs/graphql"; +import { Args, ArgsType, createUnionType, ID, Info, Int, Mutation, ObjectType, Parent, Query, ResolveField, Resolver, Union } from "@nestjs/graphql"; import { GraphQLError, GraphQLResolveInfo } from "graphql"; import { PaginatedResponseFactory } from "../common/pagination/paginated-response.factory"; @@ -140,7 +140,7 @@ export function createPageTreeResolver({ return this.pageTreeReadApi.getChildNodes(node); } - @ResolveField(() => Number) + @ResolveField(() => Int) async numberOfDescendants(@Parent() node: PageTreeNodeInterface): Promise { const childNodes = await this.pageTreeReadApi.getChildNodes(node); let numberOfDescendants = childNodes.length; diff --git a/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts b/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts index 011af80ca43..69186817dcf 100644 --- a/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts +++ b/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts @@ -19,6 +19,7 @@ export interface RedirectInterface { target: BlockDataInterface; comment?: string; active: boolean; + activatedAt?: Date; generationType: RedirectGenerationType; createdAt: Date; updatedAt: Date; @@ -62,6 +63,13 @@ export class RedirectEntityFactory { @Field() active: boolean = true; + @Property({ + columnType: "timestamp with time zone", + nullable: true, + }) + @Field({ nullable: true }) + activatedAt?: Date = new Date(); + @Enum(() => RedirectGenerationType) @Field(() => RedirectGenerationType) generationType: RedirectGenerationType; diff --git a/packages/api/cms-api/src/redirects/redirects.resolver.ts b/packages/api/cms-api/src/redirects/redirects.resolver.ts index 8949d524141..fa750ef0716 100644 --- a/packages/api/cms-api/src/redirects/redirects.resolver.ts +++ b/packages/api/cms-api/src/redirects/redirects.resolver.ts @@ -220,6 +220,7 @@ export function createRedirectsResolver({ const entity = this.repository.create({ scope: nonEmptyScopeOrNothing(scope), + activatedAt: new Date(), ...input, target: input.target.transformToBlockData(), }); @@ -256,7 +257,7 @@ export function createRedirectsResolver({ ): Promise { const redirect = await this.repository.findOneOrFail(id); - wrap(redirect).assign({ active: input.active }); + wrap(redirect).assign({ active: input.active, activatedAt: input.active ? new Date() : null }); await this.entityManager.persistAndFlush(redirect); return this.repository.findOneOrFail(id); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 428bc6952ee..119f0397064 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -17,6 +17,12 @@ ## 8.0.0-beta.0 +## 7.21.1 + +## 7.21.0 + +## 7.20.0 + ## 7.19.0 ## 7.18.0 diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md index 2eed8526e5c..172f6e84296 100644 --- a/packages/eslint-config/CHANGELOG.md +++ b/packages/eslint-config/CHANGELOG.md @@ -54,6 +54,24 @@ - a8edddb: Enable `@typescript-eslint/consistent-type-imports` rule +## 7.21.1 + +### Patch Changes + +- @comet/eslint-plugin@7.21.1 + +## 7.21.0 + +### Patch Changes + +- @comet/eslint-plugin@7.21.0 + +## 7.20.0 + +### Patch Changes + +- @comet/eslint-plugin@7.20.0 + ## 7.19.0 ### Patch Changes diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 2d22462357c..71b89061f70 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -17,6 +17,12 @@ ## 8.0.0-beta.0 +## 7.21.1 + +## 7.21.0 + +## 7.20.0 + ## 7.19.0 ## 7.18.0 diff --git a/packages/site/cms-site/CHANGELOG.md b/packages/site/cms-site/CHANGELOG.md index 3d223c2a593..6b0a7f2bd58 100644 --- a/packages/site/cms-site/CHANGELOG.md +++ b/packages/site/cms-site/CHANGELOG.md @@ -39,6 +39,28 @@ - b8817b8: Add `AdminMessageType`, `IAdminContentScopeMessage`, `IAdminGraphQLApiUrlMessage`, `IAdminHoverComponentMessage`, `IAdminShowOnlyVisibleMessage`, `IFrameHoverComponentMessage`, `IFrameLocationMessage`, `IFrameMessage`, `IFrameMessageType`, `IFrameOpenLinkMessage`, `IFrameSelectComponentMessage`, and `IReadyIFrameMessage` to the public API - b8817b8: Add `AdminMessageType`, `IAdminContentScopeMessage`, `IAdminGraphQLApiUrlMessage`, `IAdminHoverComponentMessage`, `IAdminShowOnlyVisibleMessage`, `IFrameHoverComponentMessage`, `IFrameLocationMessage`, `IFrameMessage`, `IFrameMessageType`, `IFrameOpenLinkMessage`, `IFrameSelectComponentMessage`, and `IReadyIFrameMessage` to the public API +## 7.21.1 + +### Patch Changes + +- c84874edf: Revert "Fix `PixelImageBlock` fixed height, auto width issue" added in v7.20.0 + + In v7.20.0, height was set to `100%` for `PixelImageBlock`. + This caused issues when the image was not wrapped, as it would inherit the height of the next parent element instead of maintaining its aspect ratio. + Thus, we are reverting this change to restore the previous behavior. + +## 7.21.0 + +### Patch Changes + +- 904ff5f1d: Deprecated: This package is now deprecated in favor of `@comet/site-nextjs` + +## 7.20.0 + +### Patch Changes + +- a06cac3a7: Fix `PixelImageBlock` issue when setting fixed height and width auto + ## 7.19.0 ## 7.18.0 diff --git a/packages/site/cms-site/src/blocks/PixelImageBlock.tsx b/packages/site/cms-site/src/blocks/PixelImageBlock.tsx index fd05e6874fd..4bf6d8f5691 100644 --- a/packages/site/cms-site/src/blocks/PixelImageBlock.tsx +++ b/packages/site/cms-site/src/blocks/PixelImageBlock.tsx @@ -95,6 +95,5 @@ function createDominantImageDataUrl(w: number, h: number, dominantColor = "#ffff const ImageContainer = styled.div<{ $aspectRatio: number }>` position: relative; width: 100%; - height: 100%; aspect-ratio: ${({ $aspectRatio }) => $aspectRatio}; `; diff --git a/packages/site/cms-site/src/iframebridge/useBlockPreviewFetch.tsx b/packages/site/cms-site/src/iframebridge/useBlockPreviewFetch.tsx index 2ec230ccd6a..6a9970aaf27 100644 --- a/packages/site/cms-site/src/iframebridge/useBlockPreviewFetch.tsx +++ b/packages/site/cms-site/src/iframebridge/useBlockPreviewFetch.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect, useState } from "react"; import { createFetchInMemoryCache } from "../graphQLFetch/fetchInMemoryCache"; diff --git a/packages/site/site-nextjs/.gitignore b/packages/site/site-nextjs/.gitignore new file mode 100644 index 00000000000..cf4c6a81a53 --- /dev/null +++ b/packages/site/site-nextjs/.gitignore @@ -0,0 +1,3 @@ +lib/ +block-meta.json +src/blocks.generated.ts diff --git a/packages/site/site-nextjs/.prettierignore b/packages/site/site-nextjs/.prettierignore new file mode 100644 index 00000000000..960a54081c8 --- /dev/null +++ b/packages/site/site-nextjs/.prettierignore @@ -0,0 +1,2 @@ +lib/ +block-meta.json \ No newline at end of file diff --git a/packages/site/site-nextjs/CHANGELOG.md b/packages/site/site-nextjs/CHANGELOG.md new file mode 100644 index 00000000000..b6d924fee92 --- /dev/null +++ b/packages/site/site-nextjs/CHANGELOG.md @@ -0,0 +1,38 @@ +# @comet/site-nextjs + +## 7.21.1 + +### Patch Changes + +- c84874edf: Revert "Fix `PixelImageBlock` fixed height, auto width issue" added in v7.20.0 + + In v7.20.0, height was set to `100%` for `PixelImageBlock`. + This caused issues when the image was not wrapped, as it would inherit the height of the next parent element instead of maintaining its aspect ratio. + Thus, we are reverting this change to restore the previous behavior. + + - @comet/site-react@7.21.1 + +## 7.21.0 + +### Minor Changes + +- 904ff5f1d: Introduce new package `@comet/site-nextjs` as copy of `@comet/cms-site` + + Changes: + + - Remove `styled-components` as peer dependency + - use SCSS modules instead + - `@comet/site-nextjs` is pure ESM + + To load the CSS, you need to import it like this: + + ```ts + import "@comet/site-nextjs/css"; + ``` + + In Next.js you can do that in `/app/layout.tsx`. + +### Patch Changes + +- Updated dependencies [ede41201a] + - @comet/site-react@7.21.0 diff --git a/packages/site/site-nextjs/eslint.config.mjs b/packages/site/site-nextjs/eslint.config.mjs new file mode 100644 index 00000000000..93f2725e005 --- /dev/null +++ b/packages/site/site-nextjs/eslint.config.mjs @@ -0,0 +1,28 @@ +import eslintConfigNextJs from "@comet/eslint-config/nextjs.js"; + +/** @type {import('eslint')} */ +const config = [ + { + ignores: ["src/*.generated.ts", "lib/**", "**/*.generated.ts", "block-meta.json"], + }, + ...eslintConfigNextJs, + { + rules: { + "@next/next/no-html-link-for-pages": "off", // disabled because lib has no pages dir + "@comet/no-other-module-relative-import": "off", + }, + }, + { + ignores: ["*.json"], + rules: { + "@typescript-eslint/consistent-type-exports": [ + "error", + { + fixMixedExportsWithInlineTypeSpecifier: true, + }, + ], + }, + }, +]; + +export default config; diff --git a/packages/site/site-nextjs/jest.config.ts b/packages/site/site-nextjs/jest.config.ts new file mode 100644 index 00000000000..f474b266e3b --- /dev/null +++ b/packages/site/site-nextjs/jest.config.ts @@ -0,0 +1,198 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/en/configuration.html + */ + +export default { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/9v/pgcggckn1t5g2b5qhrnpsyxc0000gn/T/jest_dx", + + // Automatically clear mock calls and instances between every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + // coverageDirectory: undefined, + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + globals: { + "ts-jest": { + tsconfig: "tsconfig.test.json", + }, + }, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + reporters: ["default", "jest-junit"], + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + rootDir: "./src", + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "jest-environment-jsdom", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/packages/site/site-nextjs/lint-staged.config.cjs b/packages/site/site-nextjs/lint-staged.config.cjs new file mode 100644 index 00000000000..b6b9d45f201 --- /dev/null +++ b/packages/site/site-nextjs/lint-staged.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + "src/**/*.{ts,tsx,js,jsx,json,css,scss,md}": () => "pnpm lint:eslint", + "src/**/*.{ts,tsx}": () => "pnpm lint:tsc", + "*.{js,json,md,yml,yaml,css,scss}": () => "pnpm lint:prettier", +}; diff --git a/packages/site/site-nextjs/package.json b/packages/site/site-nextjs/package.json new file mode 100644 index 00000000000..3c2bdaedd74 --- /dev/null +++ b/packages/site/site-nextjs/package.json @@ -0,0 +1,80 @@ +{ + "name": "@comet/site-nextjs", + "version": "7.21.1", + "repository": { + "type": "git", + "url": "https://github.com/vivid-planet/comet", + "directory": "packages/site/site-nextjs" + }, + "license": "BSD-2-Clause", + "type": "module", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js" + }, + "./css": "./lib/style.css" + }, + "module": "./lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib/*" + ], + "scripts": { + "build": "$npm_execpath run clean && npm run generate-block-types && vite build", + "clean": "rimraf lib 'src/**/*.generated.ts'", + "dev": "$npm_execpath run clean && $npm_execpath generate-block-types && vite build --watch", + "generate-block-types": "comet generate-block-types", + "generate-block-types:watch": "chokidar -s \"block-meta.json\" -c \"$npm_execpath generate-block-types\"", + "lint": "$npm_execpath generate-block-types && run-p lint:prettier lint:eslint lint:tsc", + "lint:eslint": "eslint --max-warnings 0 src/ **/*.json --no-warn-ignored", + "lint:prettier": "npx prettier --check './**/*.{js,json,md,yml,yaml,css,scss}'", + "lint:tsc": "tsc --noEmit", + "test": "jest --verbose=true --passWithNoTests", + "test:watch": "jest --watch" + }, + "dependencies": { + "@comet/site-react": "workspace:*", + "clsx": "^2.1.1", + "jose": "^5.2.4", + "rimraf": "^3.0.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@comet/cli": "workspace:*", + "@comet/eslint-config": "workspace:*", + "@types/jest": "^29.5.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.4.1", + "chokidar-cli": "^2.0.0", + "eslint": "^9.22.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^15.0.0", + "next": "^14.2.24", + "npm-run-all": "^4.1.5", + "prettier": "^2.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "rollup": "^4.40.2", + "rollup-plugin-preserve-directives": "^0.4.0", + "sass": "^1.87.0", + "ts-jest": "^29.0.5", + "typescript": "^4.0.0", + "vite": "^5.1.6", + "vite-plugin-dts": "^4.5.3" + }, + "peerDependencies": { + "next": "^14.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + } +} diff --git a/packages/site/site-nextjs/src/blocks/DamFileDownloadLinkBlock.tsx b/packages/site/site-nextjs/src/blocks/DamFileDownloadLinkBlock.tsx new file mode 100644 index 00000000000..87386c63e3d --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/DamFileDownloadLinkBlock.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { type PropsWithData, withPreview } from "@comet/site-react"; +import { cloneElement, type ReactElement } from "react"; + +import { type DamFileDownloadLinkBlockData } from "../blocks.generated"; + +interface Props extends PropsWithData { + children: ReactElement; + title?: string; + className?: string; + legacyBehavior?: boolean; +} + +export const DamFileDownloadLinkBlock = withPreview( + ({ data: { file, openFileType }, children, title, className, legacyBehavior }: Props) => { + if (!file) { + if (legacyBehavior) { + return children; + } + + return {children}; + } + + const href = file.fileUrl; + const target = openFileType === "NewTab" ? "_blank" : undefined; + + if (legacyBehavior) { + return cloneElement(children, { href, target, title }); + } + + return ( + + {children} + + ); + }, + { label: "DamFileDownloadLink" }, +); diff --git a/packages/site/site-nextjs/src/blocks/DamVideoBlock.module.scss b/packages/site/site-nextjs/src/blocks/DamVideoBlock.module.scss new file mode 100644 index 00000000000..4a9b8f3e1f8 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/DamVideoBlock.module.scss @@ -0,0 +1,9 @@ +.video { + width: 100%; + object-fit: cover; + aspect-ratio: var(--aspect-ratio); +} + +.fill { + height: 100%; +} diff --git a/packages/site/site-nextjs/src/blocks/DamVideoBlock.tsx b/packages/site/site-nextjs/src/blocks/DamVideoBlock.tsx new file mode 100644 index 00000000000..8db1b4bf6c6 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/DamVideoBlock.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { PreviewSkeleton, type PropsWithData, useIsElementInViewport, withPreview } from "@comet/site-react"; +import clsx from "clsx"; +import { type ReactElement, type ReactNode, useRef, useState } from "react"; + +import { type DamVideoBlockData } from "../blocks.generated"; +import styles from "./DamVideoBlock.module.scss"; +import { VideoPreviewImage, type VideoPreviewImageProps } from "./helpers/VideoPreviewImage"; + +interface DamVideoBlockProps extends PropsWithData { + aspectRatio?: string; + previewImageSizes?: string; + renderPreviewImage?: (props: VideoPreviewImageProps) => ReactElement; + fill?: boolean; + previewImageIcon?: ReactNode; +} + +export const DamVideoBlock = withPreview( + ({ + data: { damFile, autoplay, loop, showControls, previewImage }, + aspectRatio = "16x9", + previewImageSizes, + renderPreviewImage, + fill, + previewImageIcon, + }: DamVideoBlockProps) => { + if (damFile === undefined) { + return ; + } + + const [showPreviewImage, setShowPreviewImage] = useState(true); + const hasPreviewImage = Boolean(previewImage && previewImage.damFile); + + const videoRef = useRef(null); + + useIsElementInViewport(videoRef, (inView) => { + if (autoplay && videoRef.current) { + if (inView) { + videoRef.current.play(); + } else { + videoRef.current.pause(); + } + } + }); + + return ( + <> + {hasPreviewImage && showPreviewImage ? ( + renderPreviewImage ? ( + renderPreviewImage({ + onPlay: () => setShowPreviewImage(false), + image: previewImage, + aspectRatio, + sizes: previewImageSizes, + fill: fill, + icon: previewImageIcon, + }) + ) : ( + setShowPreviewImage(false)} + image={previewImage} + aspectRatio={aspectRatio} + sizes={previewImageSizes} + fill={fill} + icon={previewImageIcon} + /> + ) + ) : ( + + )} + + ); + }, + { label: "Video" }, +); diff --git a/packages/site/site-nextjs/src/blocks/EmailLinkBlock.tsx b/packages/site/site-nextjs/src/blocks/EmailLinkBlock.tsx new file mode 100644 index 00000000000..5522b9ac748 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/EmailLinkBlock.tsx @@ -0,0 +1,33 @@ +import { type PropsWithData } from "@comet/site-react"; +import { cloneElement, type ReactElement } from "react"; + +import { type EmailLinkBlockData } from "../blocks.generated"; + +interface EmailLinkBlockProps extends PropsWithData { + children: ReactElement; + title?: string; + className?: string; + legacyBehavior?: boolean; +} + +export const EmailLinkBlock = ({ data: { email }, children, title, className, legacyBehavior }: EmailLinkBlockProps) => { + if (!email) { + if (legacyBehavior) { + return children; + } + + return {children}; + } + + const href = `mailto:${email}`; + + if (legacyBehavior) { + return cloneElement(children, { href, title }); + } + + return ( + + {children} + + ); +}; diff --git a/packages/site/site-nextjs/src/blocks/ExternalLinkBlock.tsx b/packages/site/site-nextjs/src/blocks/ExternalLinkBlock.tsx new file mode 100644 index 00000000000..7e2b4ac6b49 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/ExternalLinkBlock.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { type PropsWithData, usePreview } from "@comet/site-react"; +import { cloneElement, type MouseEventHandler, type ReactElement } from "react"; + +import { type ExternalLinkBlockData } from "../blocks.generated"; +import { sendSitePreviewIFrameMessage } from "../sitePreview/iframebridge/sendSitePreviewIFrameMessage"; +import { SitePreviewIFrameMessageType } from "../sitePreview/iframebridge/SitePreviewIFrameMessage"; + +interface ExternalLinkBlockProps extends PropsWithData { + children: ReactElement; + title?: string; + className?: string; + legacyBehavior?: boolean; +} + +export function ExternalLinkBlock({ data: { targetUrl, openInNewWindow }, children, title, className, legacyBehavior }: ExternalLinkBlockProps) { + const preview = usePreview(); + + if (preview.previewType === "SitePreview" || preview.previewType === "BlockPreview") { + const onClick: MouseEventHandler = (event) => { + event.preventDefault(); + if (preview.previewType === "SitePreview") { + // send link to admin to handle external link + sendSitePreviewIFrameMessage({ + cometType: SitePreviewIFrameMessageType.OpenLink, + data: { link: { openInNewWindow, targetUrl } }, + }); + } + }; + + if (legacyBehavior) { + return cloneElement(children, { href: "#", onClick, title }); + } + + return ( + + {children} + + ); + } else { + if (!targetUrl) { + if (legacyBehavior) { + return children; + } + + return {children}; + } + + const href = targetUrl; + const target = openInNewWindow ? "_blank" : undefined; + + if (legacyBehavior) { + return cloneElement(children, { href, target, title }); + } + + return ( + + {children} + + ); + } +} diff --git a/packages/site/site-nextjs/src/blocks/InternalLinkBlock.tsx b/packages/site/site-nextjs/src/blocks/InternalLinkBlock.tsx new file mode 100644 index 00000000000..ea66a4497cf --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/InternalLinkBlock.tsx @@ -0,0 +1,41 @@ +"use client"; +import { type PropsWithData } from "@comet/site-react"; +import Link from "next/link"; +import { type PropsWithChildren } from "react"; + +import { type InternalLinkBlockData } from "../blocks.generated"; + +interface InternalLinkBlockProps extends PropsWithChildren> { + title?: string; + className?: string; + legacyBehavior?: boolean; +} + +/** + * @deprecated There should be a copy of this component in the application for flexibility (e.g. multi language support) + */ +export function InternalLinkBlock({ data: { targetPage, targetPageAnchor }, children, title, className, legacyBehavior }: InternalLinkBlockProps) { + if (!targetPage) { + if (legacyBehavior) { + return <>{children}; + } + + return {children}; + } + + const href = targetPageAnchor !== undefined ? `${targetPage.path}#${targetPageAnchor}` : targetPage.path; + + if (legacyBehavior) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/packages/site/site-nextjs/src/blocks/PhoneLinkBlock.tsx b/packages/site/site-nextjs/src/blocks/PhoneLinkBlock.tsx new file mode 100644 index 00000000000..5618a6ec1d2 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/PhoneLinkBlock.tsx @@ -0,0 +1,33 @@ +import { type PropsWithData } from "@comet/site-react"; +import { cloneElement, type ReactElement } from "react"; + +import { type PhoneLinkBlockData } from "../blocks.generated"; + +interface PhoneLinkBlockProps extends PropsWithData { + children: ReactElement; + title?: string; + className?: string; + legacyBehavior?: boolean; +} + +export const PhoneLinkBlock = ({ data: { phone }, children, title, className, legacyBehavior }: PhoneLinkBlockProps) => { + if (!phone) { + if (legacyBehavior) { + return children; + } + + return {children}; + } + + const href = `tel:${phone}`; + + if (legacyBehavior) { + return cloneElement(children, { href, title }); + } + + return ( + + {children} + + ); +}; diff --git a/packages/site/site-nextjs/src/blocks/PixelImageBlock.module.scss b/packages/site/site-nextjs/src/blocks/PixelImageBlock.module.scss new file mode 100644 index 00000000000..17e4b12c194 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/PixelImageBlock.module.scss @@ -0,0 +1,5 @@ +.imageContainer { + position: relative; + width: 100%; + aspect-ratio: var(--aspect-ratio); +} diff --git a/packages/site/site-nextjs/src/blocks/PixelImageBlock.tsx b/packages/site/site-nextjs/src/blocks/PixelImageBlock.tsx new file mode 100644 index 00000000000..fcd701adea8 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/PixelImageBlock.tsx @@ -0,0 +1,103 @@ +"use client"; +import { + calculateInheritAspectRatio, + generateImageUrl, + getMaxDimensionsFromArea, + type ImageDimensions, + parseAspectRatio, + PreviewSkeleton, + type PropsWithData, + withPreview, +} from "@comet/site-react"; +// eslint-disable-next-line no-restricted-imports +import NextImage, { type ImageProps } from "next/image"; + +import { type PixelImageBlockData } from "../blocks.generated"; +import styles from "./PixelImageBlock.module.scss"; + +interface PixelImageBlockProps extends PropsWithData, Omit { + aspectRatio: string | number | "inherit"; + /** + * Do not set an `alt` attribute. The alt text is set in the DAM. + */ + alt?: never; +} + +export const PixelImageBlock = withPreview( + ({ aspectRatio, data: { damFile, cropArea, urlTemplate }, fill, ...nextImageProps }: PixelImageBlockProps) => { + if (!damFile || !damFile.image) return ; + + // If we have a crop area set, DAM setting are overwritten, so we use that + const usedCropArea = cropArea ?? damFile.image.cropArea; + + let usedAspectRatio: number; + + if (aspectRatio === "inherit") { + usedAspectRatio = calculateInheritAspectRatio(damFile.image, usedCropArea); + } else { + usedAspectRatio = parseAspectRatio(aspectRatio); + } + + let dimensions: ImageDimensions; + + // check if image is cropped + if (usedCropArea.width && usedCropArea.height) { + dimensions = getMaxDimensionsFromArea( + { + width: (usedCropArea.width * damFile.image.width) / 100, + height: (usedCropArea.height * damFile.image.height) / 100, + }, + usedAspectRatio, + ); + } + // as a fallback use image dimensions + else { + dimensions = getMaxDimensionsFromArea( + { + width: damFile.image.width, + height: damFile.image.height, + }, + usedAspectRatio, + ); + } + + const blurDataUrl = createDominantImageDataUrl(dimensions.width, dimensions.height, damFile.image.dominantColor); + + const nextImage = ( + generateImageUrl(loaderProps, usedAspectRatio)} + src={urlTemplate} + fill + style={{ objectFit: "cover" }} + placeholder="blur" + blurDataURL={blurDataUrl} + alt={damFile.altText ?? ""} + {...nextImageProps} + /> + ); + + // default behavior when fill is set to true: do not wrap in container -> an own container must be used + if (fill) { + return nextImage; + } + + return ( +
+ {nextImage} +
+ ); + }, + { label: "PixelImage" }, +); + +// to be used as placeholderImage +function createDominantImageDataUrl(w: number, h: number, dominantColor = "#ffffff"): string { + const toBase64 = (str: string) => (typeof window === "undefined" ? Buffer.from(str).toString("base64") : window.btoa(str)); + + const svg = ` + + +`; + + return `data:image/svg+xml;base64,${toBase64(svg)}`; +} diff --git a/packages/site/site-nextjs/src/blocks/VimeoVideoBlock.module.scss b/packages/site/site-nextjs/src/blocks/VimeoVideoBlock.module.scss new file mode 100644 index 00000000000..f4b27ac1b81 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/VimeoVideoBlock.module.scss @@ -0,0 +1,19 @@ +.videoContainer { + overflow: hidden; + position: relative; + aspect-ratio: var(--aspect-ratio); +} + +.fill { + width: 100%; + height: 100%; +} + +.vimeoContainer { + position: absolute; + border: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; +} diff --git a/packages/site/site-nextjs/src/blocks/VimeoVideoBlock.tsx b/packages/site/site-nextjs/src/blocks/VimeoVideoBlock.tsx new file mode 100644 index 00000000000..04ce8dc2768 --- /dev/null +++ b/packages/site/site-nextjs/src/blocks/VimeoVideoBlock.tsx @@ -0,0 +1,113 @@ +"use client"; +import { PreviewSkeleton, type PropsWithData, useIsElementInViewport, withPreview } from "@comet/site-react"; +import clsx from "clsx"; +import { type ReactElement, type ReactNode, useRef, useState } from "react"; + +import { type VimeoVideoBlockData } from "../blocks.generated"; +import { VideoPreviewImage, type VideoPreviewImageProps } from "./helpers/VideoPreviewImage"; +import styles from "./VimeoVideoBlock.module.scss"; + +function parseVimeoIdentifier(vimeoIdentifier: string): string | undefined { + const urlRegEx = /^(https?:\/\/)?((www\.|player\.)?vimeo\.com\/?(showcase\/)*([0-9a-z]*\/)*([0-9]{6,11})[?]?.*)$/; + const idRegEx = /^([0-9]{6,11})$/; + + const urlRegExMatch = vimeoIdentifier.match(urlRegEx); + const idRegExMatch = vimeoIdentifier.match(idRegEx); + + if (!urlRegExMatch && !idRegExMatch) return undefined; + + if (urlRegExMatch) { + return urlRegExMatch[6]; + } else if (idRegExMatch) { + return idRegExMatch[1]; + } +} + +interface VimeoVideoBlockProps extends PropsWithData { + aspectRatio?: string; + previewImageSizes?: string; + renderPreviewImage?: (props: VideoPreviewImageProps) => ReactElement; + fill?: boolean; + previewImageIcon?: ReactNode; +} + +export const VimeoVideoBlock = withPreview( + ({ + data: { vimeoIdentifier, autoplay, loop, showControls, previewImage }, + aspectRatio = "16x9", + previewImageSizes, + renderPreviewImage, + fill, + previewImageIcon, + }: VimeoVideoBlockProps) => { + const [showPreviewImage, setShowPreviewImage] = useState(true); + const hasPreviewImage = !!(previewImage && previewImage.damFile); + const inViewRef = useRef(null); + const iframeRef = useRef(null); + + const handleVisibilityChange = (isVisible: boolean) => { + if (iframeRef.current?.contentWindow) { + iframeRef.current.contentWindow.postMessage( + JSON.stringify({ method: isVisible && autoplay ? "play" : "pause" }), + "https://player.vimeo.com", + ); + } + }; + + useIsElementInViewport(inViewRef, handleVisibilityChange); + + if (!vimeoIdentifier) { + return ; + } + + const identifier = parseVimeoIdentifier(vimeoIdentifier); + + const searchParams = new URLSearchParams(); + if (autoplay) searchParams.append("muted", "1"); + + if (loop !== undefined) searchParams.append("loop", Number(loop).toString()); + + if (showControls !== undefined) searchParams.append("controls", Number(showControls).toString()); + + searchParams.append("dnt", "1"); + + const vimeoBaseUrl = "https://player.vimeo.com/video/"; + const vimeoUrl = new URL(`${vimeoBaseUrl}${identifier ?? ""}`); + vimeoUrl.search = searchParams.toString(); + + return ( + <> + {hasPreviewImage && showPreviewImage ? ( + renderPreviewImage ? ( + renderPreviewImage({ + onPlay: () => setShowPreviewImage(false), + image: previewImage, + aspectRatio, + sizes: previewImageSizes, + fill: fill, + icon: previewImageIcon, + }) + ) : ( + setShowPreviewImage(false)} + image={previewImage} + aspectRatio={aspectRatio} + sizes={previewImageSizes} + fill={fill} + icon={previewImageIcon} + /> + ) + ) : ( +
+