Merge pull request #15 from keycloakify/vite

Vite
This commit is contained in:
Joseph Garrone 2024-02-13 02:07:38 +01:00 committed by GitHub
commit 8a1a9b1026
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 4327 additions and 11620 deletions

25
.eslintrc.cjs Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react-hooks/exhaustive-deps': 'off',
'@typescript-eslint/no-redeclare': 'off',
'no-labels': 'off',
},
overrides: [
{
files: ['**/*.stories.*'],
rules: {
'import/no-anonymous-default-export': 'off',
},
},
],
}

View File

@ -49,8 +49,8 @@ jobs:
- run: npx keycloakify - run: npx keycloakify
env: env:
XDG_CACHE_HOME: "/home/runner/.cache/yarn" XDG_CACHE_HOME: "/home/runner/.cache/yarn"
- run: mv build_keycloak/target/retrocompat-*.jar retrocompat-keycloak-theme.jar - run: mv dist_keycloak/target/retrocompat-*.jar retrocompat-keycloak-theme.jar
- run: mv build_keycloak/target/*.jar keycloak-theme.jar - run: mv dist_keycloak/target/*.jar keycloak-theme.jar
- uses: softprops/action-gh-release@v1 - uses: softprops/action-gh-release@v1
with: with:
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }} name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}

2
.gitignore vendored
View File

@ -52,6 +52,6 @@ jspm_packages
/dist /dist
/build_keycloak /dist_keycloak
/build /build
/storybook-static /storybook-static

View File

@ -1,16 +0,0 @@
module.exports = {
"stories": [
"../src/**/*.stories.tsx",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app"
],
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-webpack5"
},
"staticDirs": ['../public']
}

20
.storybook/main.ts Normal file
View File

@ -0,0 +1,20 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
staticDirs: ["../public"]
};
export default config;

View File

@ -1,13 +0,0 @@
export const parameters = {
actions: {argTypesRegex: "^on[A-Z].*"},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: (a, b) =>
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, {numeric: true}),
},
}

15
.storybook/preview.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Preview } from "@storybook/react";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@ -8,6 +8,6 @@ RUN yarn build
# production environment # production environment
FROM nginx:stable-alpine FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf
CMD nginx -g 'daemon off;' CMD nginx -g 'daemon off;'

View File

@ -10,13 +10,10 @@
# Introduction # Introduction
This repo constitutes an easily reusable setup for a Keycloak theme project OR for a SPA React App that generates a This repo constitutes an easily reusable setup for a Keycloak theme project OR for a Vite SPA React App that generates a
Keycloak theme that goes along with it. Keycloak theme that goes along with it.
If you are only looking to create a Keycloak theme (and not a Keycloak theme and an App that share the same codebase) there are a lot of things that you can remove from this starter: [Please read this section of the README](#standalone-keycloak-theme). If you are only looking to create a Keycloak theme (and not a Keycloak theme and an App that share the same codebase) there are a lot of things that you can remove from this starter: [Please read this section of the README](#standalone-keycloak-theme).
> ❗️ WARNING ❗️: Don't waste time trying to port this setup to [Vite](https://vitejs.dev/).
> Vite support is comming in a matter of days. There will be a comprehensive migration guide.
# Quick start # Quick start
```bash ```bash
@ -32,7 +29,7 @@ yarn storybook # Start Storybook
# You can create stories even for pages that you haven't explicitly overloaded. See src/keycloak-theme/login/pages/LoginResetPassword.stories.tsx # You can create stories even for pages that you haven't explicitly overloaded. See src/keycloak-theme/login/pages/LoginResetPassword.stories.tsx
# See Keycloakify's storybook for if you need a starting point for your stories: https://github.com/keycloakify/keycloakify/tree/main/stories # See Keycloakify's storybook for if you need a starting point for your stories: https://github.com/keycloakify/keycloakify/tree/main/stories
yarn start # See the Hello World app yarn dev # See the Hello World app
# Uncomment line 97 of src/keycloak-theme/login/kcContext where it reads: `mockPageId: "login.ftl"`, reload https://localhost:3000 # Uncomment line 97 of src/keycloak-theme/login/kcContext where it reads: `mockPageId: "login.ftl"`, reload https://localhost:3000
# You can now see the login.ftl page with the mock data. (Don't forget to comment it back when you're done) # You can now see the login.ftl page with the mock data. (Don't forget to comment it back when you're done)
@ -53,7 +50,7 @@ npx initialize-email-theme # For initializing your email theme
# Note that Keycloakify does not feature React integration for email yet. # Note that Keycloakify does not feature React integration for email yet.
npx download-builtin-keycloak-theme # For downloading the default theme (as a reference) npx download-builtin-keycloak-theme # For downloading the default theme (as a reference)
# Look for the files in build_keycloak/src/main/resources/theme/{base,keycloak} # Look for the files in dist_keycloak/src/main/resources/theme/{base,keycloak}
``` ```
# Theme variant # Theme variant
@ -205,8 +202,8 @@ jobs:
- run: npx keycloakify - run: npx keycloakify
env: env:
XDG_CACHE_HOME: "/home/runner/.cache/yarn" XDG_CACHE_HOME: "/home/runner/.cache/yarn"
- run: mv build_keycloak/target/retrocompat-*.jar retrocompat-keycloak-theme.jar - run: mv dist_keycloak/target/retrocompat-*.jar retrocompat-keycloak-theme.jar
- run: mv build_keycloak/target/*.jar keycloak-theme.jar - run: mv dist_keycloak/target/*.jar keycloak-theme.jar
- uses: softprops/action-gh-release@v1 - uses: softprops/action-gh-release@v1
with: with:
name: Release v\${{ needs.check_if_version_upgraded.outputs.to_version }} name: Release v\${{ needs.check_if_version_upgraded.outputs.to_version }}

76
index.html Normal file
View File

@ -0,0 +1,76 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--
Notice the use of %BASE_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%BASE_URL%favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
-->
<link rel="icon" type="image/png" sizes="32x32" href="%BASE_URL%favicon-32x32.png">
<title>Keycloakify starter</title>
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
<!-- SEE: https://docs.keycloakify.dev/limitations#self-hosted-fonts -->
<!-- Don't forget to import your custom fonts in Storybook as well: https://github.com/keycloakify/keycloakify-starter/blob/bb019e66fb09166cb9af1e24e230994f59daa420/src/keycloak-theme/login/createPageStory.tsx#L21 -->
<style>
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: normal;
/*400*/
font-display: swap;
src: url("%BASE_URL%fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("%BASE_URL%fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("%BASE_URL%fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: bold;
/*700*/
font-display: swap;
src: url("%BASE_URL%fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
}
</style>
<meta name="keycloakify-ignore-start">
<script>console.log("This is logged Only in the main app, stripped out in the theme")</script>
<meta name="keycloakify-ignore-end">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -1,86 +1,64 @@
{ {
"name": "keycloakify-starter", "name": "keycloakify-starter",
"homepage": "https://starter.keycloakify.dev", "homepage": "https://starter.keycloakify.dev",
"version": "5.3.3", "version": "5.2.0",
"description": "A starter/demo project for keycloakify", "description": "A starter/demo project for keycloakify",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/codegouvfr/keycloakify-starter.git" "url": "git://github.com/codegouvfr/keycloakify-starter.git"
}, },
"scripts": { "type": "module",
"start": "copy-keycloak-resources-to-public && react-scripts start", "scripts": {
"storybook": "copy-keycloak-resources-to-public && start-storybook -p 6006", "dev": "vite",
"build": "react-scripts build && rimraf build/keycloak-resources", "build": "tsc && vite build",
"build-keycloak-theme": "yarn build && keycloakify" "build-keycloak-theme": "yarn build && keycloakify",
}, "storybook": "storybook dev -p 6006",
"keycloakify": { "build-storybook": "storybook build"
"themeName": "keycloakify-starter", },
"extraThemeProperties": [ "keycloakify": {
"foo=bar" "themeName": "keycloakify-starter",
] "extraThemeProperties": [
}, "foo=bar"
"author": "u/garronej", ]
"license": "MIT", },
"keywords": [], "author": "u/garronej",
"dependencies": { "license": "MIT",
"evt": "^2.5.7", "keywords": [],
"oidc-spa": "^4.2.0", "dependencies": {
"keycloakify": "^9.3.0", "evt": "^2.5.7",
"powerhooks": "^1.0.8", "keycloakify": "^9.4.1",
"react": "18.1.0", "oidc-spa": "^4.2.1",
"react-dom": "18.1.0", "powerhooks": "^1.0.8",
"tsafe": "^1.6.6", "react": "^18.2.0",
"zod": "^3.22.4" "react-dom": "^18.2.0",
}, "tsafe": "^1.6.6",
"devDependencies": { "zod": "^3.22.4"
"@types/node": "^15.3.1", },
"@types/react": "18.0.9", "devDependencies": {
"@types/react-dom": "18.0.4", "@storybook/addon-essentials": "^7.6.14",
"react-scripts": "5.0.1", "@storybook/addon-interactions": "^7.6.14",
"typescript": "~4.7.0", "@storybook/addon-links": "^7.6.14",
"@storybook/addon-actions": "^6.5.16", "@storybook/addon-onboarding": "^1.0.11",
"@storybook/addon-essentials": "^6.5.16", "@storybook/blocks": "^7.6.14",
"@storybook/addon-interactions": "^6.5.16", "@storybook/react": "^7.6.14",
"@storybook/addon-links": "^6.5.16", "@storybook/react-vite": "^7.6.14",
"@storybook/builder-webpack5": "^6.5.16", "@storybook/test": "^7.6.14",
"@storybook/manager-webpack5": "^6.5.16", "@types/react": "^18.2.43",
"@storybook/node-logger": "^6.5.16", "@types/react-dom": "^18.2.17",
"@storybook/preset-create-react-app": "^4.1.2", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@storybook/react": "^6.5.16", "@typescript-eslint/parser": "^6.14.0",
"@storybook/testing-library": "^0.0.13", "@vitejs/plugin-react": "^4.2.1",
"rimraf": "^5.0.5" "eslint": "^8.55.0",
}, "eslint-plugin-react-hooks": "^4.6.0",
"eslintConfig": { "eslint-plugin-react-refresh": "^0.4.5",
"extends": [ "eslint-plugin-storybook": "^0.6.15",
"react-app", "storybook": "^7.6.14",
"react-app/jest" "typescript": "^5.2.2",
], "vite": "^5.0.8",
"rules": { "vite-plugin-commonjs": "^0.10.1"
"react-hooks/exhaustive-deps": "off", },
"@typescript-eslint/no-redeclare": "off", "_comment": "See https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092",
"no-labels": "off" "resolutions": {
}, "jackspeak": "2.1.1"
"overrides": [ }
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
} }

View File

@ -1,39 +1,36 @@
/* /*
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous"> This file is only meant to be used by Storybook
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/fonts/WorkSans/font.css">
*/ */
@font-face { @font-face {
font-family: "Work Sans"; font-family: "Work Sans";
font-style: normal; font-style: normal;
font-weight: normal; /*400*/ font-weight: normal; /*400*/
font-display: swap; font-display: swap;
src: url("./worksans-regular-webfont.woff2") format("woff2"); src: url("./worksans-regular-webfont.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: "Work Sans"; font-family: "Work Sans";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url("./worksans-medium-webfont.woff2") format("woff2"); src: url("./worksans-medium-webfont.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: "Work Sans"; font-family: "Work Sans";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url("./worksans-semibold-webfont.woff2") format("woff2"); src: url("./worksans-semibold-webfont.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: "Work Sans"; font-family: "Work Sans";
font-style: normal; font-style: normal;
font-weight: bold; /*700*/ font-weight: bold; /*700*/
font-display: swap; font-display: swap;
src: url("./worksans-bold-webfont.woff2") format("woff2"); src: url("./worksans-bold-webfont.woff2") format("woff2");
} }

View File

@ -1,89 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<title>Keycloakify starter</title>
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="%PUBLIC_URL%/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
<!-- SEE: https://docs.keycloakify.dev/limitations#self-hosted-fonts -->
<!-- Don't forget to import your custom fonts in Storybook as well: https://github.com/keycloakify/keycloakify-starter/blob/bb019e66fb09166cb9af1e24e230994f59daa420/src/keycloak-theme/login/createPageStory.tsx#L21 -->
<style>
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: normal;
/*400*/
font-display: swap;
src: url("%PUBLIC_URL%/fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("%PUBLIC_URL%/fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("%PUBLIC_URL%/fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: bold;
/*700*/
font-display: swap;
src: url("%PUBLIC_URL%/fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
}
</style>
<meta name="keycloakify-ignore-start">
<script>console.log("This is logged Only in the main app, stripped out in the theme")</script>
<meta name="keycloakify-ignore-end">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -22,7 +22,7 @@ export const { OidcProvider, useOidc } = createReactOidc({
"ui_locales": "en", "ui_locales": "en",
"my_custom_param": "value of foo transferred to login page" "my_custom_param": "value of foo transferred to login page"
}), }),
publicUrl: process.env.PUBLIC_URL, publicUrl: import.meta.env.BASE_URL,
decodedIdTokenSchema: z.object({ decodedIdTokenSchema: z.object({
// Use https://jwt.io/ to tell what's in your idToken // Use https://jwt.io/ to tell what's in your idToken
// It will depend of your Keycloak configuration. // It will depend of your Keycloak configuration.

View File

@ -1,31 +0,0 @@
import { kcContext as kcLoginThemeContext } from "keycloak-theme/login/kcContext";
import { kcContext as kcAccountThemeContext } from "keycloak-theme/login/kcContext";
/**
* If you need to use process.env.PUBLIC_URL, use this variable instead.
* If you can, import your assets using the import statement.
*
* See: https://docs.keycloakify.dev/importing-assets#importing-custom-assets-that-arent-fonts
*/
export const PUBLIC_URL = (()=>{
const kcContext = (()=>{
if( kcLoginThemeContext !== undefined ){
return kcLoginThemeContext;
}
if( kcAccountThemeContext !== undefined ){
return kcLoginThemeContext
}
return undefined;
})();
return (kcContext === undefined || process.env.NODE_ENV === "development")
? process.env.PUBLIC_URL
: `${kcContext.url.resourcesPath}/build`;
})();

View File

@ -10,9 +10,9 @@ const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2")); const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
const Fallback = lazy(()=> import("keycloakify/account")); const Fallback = lazy(()=> import("keycloakify/account"));
const classes: PageProps<any, any>["classes"] = { const classes = {
"kcBodyClass": "my-root-account-class" "kcBodyClass": "my-root-account-class"
}; } satisfies PageProps["classes"];
export default function KcApp(props: { kcContext: KcContext; }) { export default function KcApp(props: { kcContext: KcContext; }) {

View File

@ -15,7 +15,13 @@ export function createPageStory<PageId extends KcContext["pageId"]>(params: {
storyPartialKcContext: params.kcContext storyPartialKcContext: params.kcContext
}); });
return <KcApp kcContext={kcContext} />; return (
<>
{/* If you import custom fonts in your index.html you have to import them in storybook as well*/}
<link rel="stylesheet" type="text/css" href={`${import.meta.env.BASE_URL}fonts/WorkSans/font.css`} />
<KcApp kcContext={kcContext} />
</>
);
} }

View File

@ -1,18 +1,22 @@
import { ComponentStory, ComponentMeta } from "@storybook/react"; import { Meta, StoryObj } from '@storybook/react';
import { createPageStory } from "../createPageStory"; import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({ const { PageStory } = createPageStory({
pageId: "password.ftl" pageId: "password.ftl"
}); });
export default { const meta = {
title: "account/Password", title: "account/Password",
component: PageStory, component: PageStory,
} as ComponentMeta<typeof PageStory>; } satisfies Meta<typeof PageStory>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: ComponentStory<typeof PageStory> = () => <PageStory export const Default: Story = {
kcContext={{ render: () => <PageStory
message: { type: "success", summary: "This is a test message" } kcContext={{
}} message: { type: "success", summary: "This is a test message" }
/>; }}
/>
};

View File

@ -16,11 +16,11 @@ const Info = lazy(() => import("keycloakify/login/pages/Info"));
// This is like adding classes to theme.properties // This is like adding classes to theme.properties
// https://github.com/keycloak/keycloak/blob/11.0.3/themes/src/main/resources/theme/keycloak/login/theme.properties // https://github.com/keycloak/keycloak/blob/11.0.3/themes/src/main/resources/theme/keycloak/login/theme.properties
const classes: PageProps<any, any>["classes"] = { const classes = {
// NOTE: The classes are defined in ./KcApp.css // NOTE: The classes are defined in ./KcApp.css
"kcHtmlClass": "my-root-class", "kcHtmlClass": "my-root-class",
"kcHeaderWrapperClass": "my-color my-font" "kcHeaderWrapperClass": "my-color my-font"
}; } satisfies PageProps["classes"];
export default function KcApp(props: { kcContext: KcContext; }) { export default function KcApp(props: { kcContext: KcContext; }) {

View File

@ -9,7 +9,6 @@ import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "./kcContext"; import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n"; import type { I18n } from "./i18n";
import keycloakifyLogoPngUrl from "./assets/keycloakify-logo.png"; import keycloakifyLogoPngUrl from "./assets/keycloakify-logo.png";
import { PUBLIC_URL } from "../../PUBLIC_URL";
export default function Template(props: TemplateProps<KcContext, I18n>) { export default function Template(props: TemplateProps<KcContext, I18n>) {
const { const {
@ -61,12 +60,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
style={{ "fontFamily": '"Work Sans"' }} style={{ "fontFamily": '"Work Sans"' }}
> >
{/* {/*
This is just to show you how it can be done but this is not the best option for importing assets. Here we are referencing the `keycloakify-logo.png` in the `public` directory.
See: https://docs.keycloakify.dev/importing-assets#importing-custom-assets When possible don't use this approach, instead ...
*/} */}
<img src={`${PUBLIC_URL}/keycloakify-logo.png`} alt="Keycloakify logo" width={50} /> <img src={`${import.meta.env.BASE_URL}keycloakify-logo.png`} alt="Keycloakify logo" width={50} />
{msg("loginTitleHtml", realm.displayNameHtml)}!!! {msg("loginTitleHtml", realm.displayNameHtml)}!!!
{/* This is the preferred way to use assets */} {/* ...rely on the bundler to import your assets, it's more efficient */}
<img src={keycloakifyLogoPngUrl} alt="Keycloakify logo" width={50} /> <img src={keycloakifyLogoPngUrl} alt="Keycloakify logo" width={50} />
</div> </div>
</div> </div>
@ -77,14 +76,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<div id="kc-locale"> <div id="kc-locale">
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}> <div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div className="kc-dropdown" id="kc-locale-dropdown"> <div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link"> <a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]} {labelBySupportedLanguageTag[currentLanguageTag]}
</a> </a>
<ul> <ul>
{locale.supported.map(({ languageTag }) => ( {locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item"> <li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={() => changeLocale(languageTag)}> <a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]} {labelBySupportedLanguageTag[languageTag]}
</a> </a>
@ -182,7 +179,6 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
> >
<div className={getClassName("kcFormGroupClass")}> <div className={getClassName("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" /> <input type="hidden" name="tryAnotherWay" value="on" />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a <a
href="#" href="#"
id="try-another-way" id="try-another-way"

View File

@ -18,7 +18,7 @@ export function createPageStory<PageId extends KcContext["pageId"]>(params: {
return ( return (
<> <>
{/* If you import custom fonts in your index.html you have to import them in storybook as well*/} {/* If you import custom fonts in your index.html you have to import them in storybook as well*/}
<link rel="stylesheet" type="text/css" href="fonts/WorkSans/font.css" /> <link rel="stylesheet" type="text/css" href={`${import.meta.env.BASE_URL}fonts/WorkSans/font.css`} />
<KcApp kcContext={kcContext} /> <KcApp kcContext={kcContext} />
</> </>
); );

View File

@ -1,98 +1,83 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { createPageStory } from "../createPageStory"; import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({ const { PageStory } = createPageStory({
pageId: "login.ftl" pageId: "login.ftl"
}); });
export default { const meta = {
title: "login/Login", title: "login/Login",
component: PageStory, component: PageStory,
} as ComponentMeta<typeof PageStory>; } satisfies Meta<typeof PageStory>;
export const Default: ComponentStory<typeof PageStory> = () => <PageStory />; export default meta;
type Story = StoryObj<typeof meta>;
export const WithoutPasswordField: ComponentStory<typeof PageStory> = () => ( export const Default: Story = {
<PageStory render: () => <PageStory />,
kcContext={{ };
realm: { password: false }
}}
/>
);
export const WithoutRegistration: ComponentStory<typeof PageStory> = () => ( export const WithoutPasswordField: Story = {
<PageStory render: () => <PageStory kcContext={{ realm: { password: false } }} />,
kcContext={{ };
realm: { registrationAllowed: false }
}}
/>
);
export const WithoutRememberMe: ComponentStory<typeof PageStory> = () => ( export const WithoutRegistration: Story = {
<PageStory render: () => <PageStory kcContext={{ realm: { registrationAllowed: false } }} />,
kcContext={{ };
realm: { rememberMe: false }
}}
/>
);
export const WithoutPasswordReset: ComponentStory<typeof PageStory> = () => ( export const WithoutRememberMe: Story = {
<PageStory render: () => <PageStory kcContext={{ realm: { rememberMe: false } }} />,
kcContext={{ };
realm: { resetPasswordAllowed: false }
}}
/>
);
export const WithEmailAsUsername: ComponentStory<typeof PageStory> = () => ( export const WithoutPasswordReset: Story = {
<PageStory render: () => <PageStory kcContext={{ realm: { resetPasswordAllowed: false } }} />,
kcContext={{ };
realm: { loginWithEmailAllowed: false }
}}
/>
);
export const WithPresetUsername: ComponentStory<typeof PageStory> = () => ( export const WithEmailAsUsername: Story = {
<PageStory render: () => <PageStory kcContext={{ realm: { loginWithEmailAllowed: false } }} />,
kcContext={{ };
login: { username: "max.mustermann@mail.com" }
}}
/>
);
export const WithImmutablePresetUsername: ComponentStory<typeof PageStory> = () => ( export const WithPresetUsername: Story = {
<PageStory render: () => <PageStory kcContext={{ login: { username: "max.mustermann@mail.com" } }} />,
kcContext={{ };
auth: {
attemptedUsername: "max.mustermann@mail.com",
showUsername: true
},
usernameHidden: true,
message: { type: "info", summary: "Please re-authenticate to continue" }
}}
/>
);
export const WithSocialProviders: ComponentStory<typeof PageStory> = () => ( export const WithImmutablePresetUsername: Story = {
<PageStory render: () => (
kcContext={{ <PageStory
social: { kcContext={{
displayInfo: true, providers: [ auth: {
{ loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google' }, attemptedUsername: "max.mustermann@mail.com",
{ loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft' }, showUsername: true,
{ loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook' }, },
{ loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram' }, usernameHidden: true,
{ loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter' }, message: { type: "info", summary: "Please re-authenticate to continue" },
{ loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn' }, }}
{ loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow' }, />
{ loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github' }, ),
{ loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab' }, };
{ loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket' },
{ loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal' },
{ loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift' },
] export const WithSocialProviders: Story = {
} render: () => (
}} <PageStory
/> kcContext={{
); social: {
displayInfo: true,
providers: [
{ loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google' },
{ loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft' },
{ loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook' },
{ loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram' },
{ loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter' },
{ loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn' },
{ loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow' },
{ loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github' },
{ loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab' },
{ loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket' },
{ loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal' },
{ loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift' },
],
},
}}
/>
),
};

View File

@ -1,23 +1,30 @@
//This is to show that you can create stories for pages that you haven't overloaded. //This is to show that you can create stories for pages that you haven't overloaded.
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { createPageStory } from "../createPageStory"; import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({ const { PageStory } = createPageStory({
pageId: "login-reset-password.ftl" pageId: "login-reset-password.ftl"
}); });
export default { const meta = {
title: "login/LoginResetPassword", title: "login/LoginResetPassword",
component: PageStory, component: PageStory,
} as ComponentMeta<typeof PageStory>; } satisfies Meta<typeof PageStory>;
export const Default: ComponentStory<typeof PageStory> = () => <PageStory />; export default meta;
type Story = StoryObj<typeof meta>;
export const WithEmailAsUsername: ComponentStory<typeof PageStory> = () => ( export const Default: Story = {
<PageStory render: () => <PageStory />
kcContext={{ };
realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true }
}} export const WithEmailAsUsername: Story = {
/> render: () => (
); <PageStory
kcContext={{
realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true }
}}
/>
)
};

View File

@ -1,21 +1,28 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { createPageStory } from "../createPageStory"; import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({ const { PageStory } = createPageStory({
pageId: "my-extra-page-2.ftl" pageId: "my-extra-page-2.ftl"
}); });
export default { const meta = {
title: "login/MyExtraPage2", title: "login/MyExtraPage2",
component: PageStory, component: PageStory,
} as ComponentMeta<typeof PageStory>; } satisfies Meta<typeof PageStory>;
export const Default: ComponentStory<typeof PageStory> = () => <PageStory />; export default meta;
type Story = StoryObj<typeof meta>;
export const WitAbc: ComponentStory<typeof PageStory> = () => ( export const Default: Story = {
<PageStory render: () => <PageStory />
kcContext={{ };
someCustomValue: "abc"
}} export const WitAbc: Story = {
/> render: () => (
); <PageStory
kcContext={{
someCustomValue: "abc"
}}
/>
)
};

View File

@ -1,13 +0,0 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({
pageId: "terms.ftl"
});
export default {
title: "login/Terms",
component: PageStory,
} as ComponentMeta<typeof PageStory>;
export const Primary: ComponentStory<typeof PageStory> = () => <PageStory />;

View File

@ -1,13 +1,18 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { createPageStory } from "../createPageStory"; import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({ const { PageStory } = createPageStory({
pageId: "terms.ftl" pageId: "terms.ftl"
}); });
export default { const meta = {
title: "login/Terms", title: "login/Terms",
component: PageStory, component: PageStory,
} as ComponentMeta<typeof PageStory>; } satisfies Meta<typeof PageStory>;
export const Primary: ComponentStory<typeof PageStory> = () => <PageStory />; export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
render: () => <PageStory />
};

View File

@ -7,8 +7,6 @@ import { evtTermMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { useDownloadTerms } from "keycloakify/login"; import { useDownloadTerms } from "keycloakify/login";
import tos_en_url from "../assets/tos_en.md";
import tos_fr_url from "../assets/tos_fr.md";
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl" }>, I18n>) { export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -28,18 +26,11 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
const tos_url = (() => { const tos_url = (() => {
switch (currentLanguageTag) { switch (currentLanguageTag) {
case "fr": return tos_fr_url; case "fr": return `${import.meta.env.BASE_URL}terms/fr.md`;
default: return tos_en_url; default: return `${import.meta.env.BASE_URL}terms/en.md`;
} }
})(); })();
if ("__STORYBOOK_ADDONS" in window) {
// NOTE: In storybook, when you import a .md file you get the content of the file.
// In Create React App on the other hand you get an url to the file.
return tos_url;
}
const markdownString = await fetch(tos_url).then(response => response.text()); const markdownString = await fetch(tos_url).then(response => response.text());
return markdownString; return markdownString;

View File

@ -32,3 +32,4 @@ createRoot(document.getElementById("root")!).render(
</Suspense> </Suspense>
</StrictMode> </StrictMode>
); );

View File

@ -1,4 +1,5 @@
/// <reference types="react-scripts" /> /// <reference types="vite/client" />
declare module "*.md" { declare module "*.md" {
const src: string; const src: string;
export default src; export default src;

View File

@ -1,21 +1,25 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"], "useDefineForClassFields": true,
"baseUrl": "src", "lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": true, "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true, /* Bundler mode */
"strict": true, "moduleResolution": "bundler",
"forceConsistentCasingInFileNames": true, "allowImportingTsExtensions": true,
"noFallthroughCasesInSwitch": true, "resolveJsonModule": true,
"module": "esnext", "isolatedModules": true,
"moduleResolution": "node", "noEmit": true,
"resolveJsonModule": true, "jsx": "react-jsx",
"isolatedModules": true,
"noEmit": true, /* Linting */
"jsx": "react-jsx" "strict": true,
}, "noUnusedLocals": true,
"include": ["src"] "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
} }

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

15
vite.config.ts Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// NOTE: This is just for the Keycloakify core contributors to be able to dynamically link
// to a local version of the keycloakify package. This is not needed for normal usage.
import commonjs from "vite-plugin-commonjs";
import { keycloakify } from "keycloakify/vite-plugin";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
commonjs(),
keycloakify()
]
})

15061
yarn.lock

File diff suppressed because it is too large Load Diff