diff --git a/gdpr-cookie-consent/.eslintrc.cjs b/gdpr-cookie-consent/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/gdpr-cookie-consent/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/gdpr-cookie-consent/.eslintrc.js b/gdpr-cookie-consent/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/gdpr-cookie-consent/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/gdpr-cookie-consent/.gitignore b/gdpr-cookie-consent/.gitignore index 3f7bf98d..80ec311f 100644 --- a/gdpr-cookie-consent/.gitignore +++ b/gdpr-cookie-consent/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/gdpr-cookie-consent/README.md b/gdpr-cookie-consent/README.md index 6fad5e99..dda7ac1d 100644 --- a/gdpr-cookie-consent/README.md +++ b/gdpr-cookie-consent/README.md @@ -1,7 +1,6 @@ # GDPR Cookie Consent -Create a simple GDPR consent form. -Till the user doesn't click on the `Accept` button, she will see a banner prompting to accept cookies. +Create a simple GDPR consent form, displayed until the button click the `Accept` button. ## Preview @@ -14,7 +13,7 @@ Open this example on [CodeSandbox](https://codesandbox.com): Users will be presented with a GDPR consent form on every page [app/root.tsx](app/root.tsx) till they submit the accept button. Once they submit the consent form, a dummy tracking [script](public/dummy-analytics-script.js) at the [app/root.tsx](app/root.tsx) will start tracking the user's data (open the browser console too see the dummy tracking message). -> If you want to reset the example delete the `gdpr-consent` cookie in the `Application`/`cookies` in the browser's developer tools. +> If you want to reset the example, delete the `gdpr-consent` cookie in the `Application`/`cookies` in the browser's developer tools. The example is using [Remix Cookie API](https://remix.run/utils/cookies). diff --git a/gdpr-cookie-consent/app/cookies.ts b/gdpr-cookie-consent/app/cookies.server.ts similarity index 100% rename from gdpr-cookie-consent/app/cookies.ts rename to gdpr-cookie-consent/app/cookies.server.ts diff --git a/gdpr-cookie-consent/app/root.tsx b/gdpr-cookie-consent/app/root.tsx index 9aa5e3fc..a8f6dc11 100644 --- a/gdpr-cookie-consent/app/root.tsx +++ b/gdpr-cookie-consent/app/root.tsx @@ -1,35 +1,34 @@ -import type { LoaderArgs, MetaFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; +import type { LoaderFunctionArgs } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, + json, useFetcher, - useLoaderData, + useRouteLoaderData, } from "@remix-run/react"; -import * as React from "react"; +import { useEffect } from "react"; -import { gdprConsent } from "~/cookies"; +import { gdprConsent } from "~/cookies.server"; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const cookieHeader = request.headers.get("Cookie"); const cookie = (await gdprConsent.parse(cookieHeader)) || {}; + return json({ track: cookie.gdprConsent }); }; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); +export function Layout({ children }: { children: React.ReactNode }) { + // We use `useRouteLoaderData` here instead of `useLoaderData` because + // the component will also be used by the + // if an error is thrown somewhere in the app, and we can't call + // `useLoaderData()` while rendering an . + const rootLoaderData = useRouteLoaderData<{ track?: true }>("root"); + const track = rootLoaderData?.track ?? false; -export default function App() { - const { track } = useLoaderData(); - const analyticsFetcher = useFetcher(); - React.useEffect(() => { + useEffect(() => { if (track) { const script = document.createElement("script"); script.src = "/dummy-analytics-script.js"; @@ -37,14 +36,18 @@ export default function App() { } }, [track]); + const analyticsFetcher = useFetcher(); + return ( + + - + {children} {track ? null : (
- ); } + +export default function App() { + return ; +} diff --git a/gdpr-cookie-consent/app/routes/enable-analytics.tsx b/gdpr-cookie-consent/app/routes/enable-analytics.tsx index 18c20d62..a8d6c4c9 100644 --- a/gdpr-cookie-consent/app/routes/enable-analytics.tsx +++ b/gdpr-cookie-consent/app/routes/enable-analytics.tsx @@ -1,9 +1,9 @@ -import type { ActionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { gdprConsent } from "~/cookies"; +import { gdprConsent } from "~/cookies.server"; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const cookieHeader = request.headers.get("Cookie"); const cookie = (await gdprConsent.parse(cookieHeader)) || {}; diff --git a/gdpr-cookie-consent/package.json b/gdpr-cookie-consent/package.json index a437590d..1314c27c 100644 --- a/gdpr-cookie-consent/package.json +++ b/gdpr-cookie-consent/package.json @@ -1,29 +1,40 @@ { + "name": "gdpr-cookie-consent", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/gdpr-cookie-consent/remix.config.js b/gdpr-cookie-consent/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/gdpr-cookie-consent/remix.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - future: { - v2_routeConvention: true, - }, - ignoredRouteFiles: ["**/.*"], - // appDirectory: "app", - // assetsBuildDirectory: "public/build", - // publicPath: "/build/", - // serverBuildPath: "build/index.js", -}; diff --git a/gdpr-cookie-consent/remix.env.d.ts b/gdpr-cookie-consent/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/gdpr-cookie-consent/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/gdpr-cookie-consent/tsconfig.json b/gdpr-cookie-consent/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/gdpr-cookie-consent/tsconfig.json +++ b/gdpr-cookie-consent/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "module": "ESNext", + "moduleResolution": "Bundler", "resolveJsonModule": true, - "target": "ES2019", + "target": "ES2022", "strict": true, "allowJs": true, + "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { "~/*": ["./app/*"] }, - // Remix takes care of building everything in `remix build`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/gdpr-cookie-consent/vite.config.ts b/gdpr-cookie-consent/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/gdpr-cookie-consent/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +});