diff --git a/src/login/KcPage.tsx b/src/login/KcPage.tsx index cfd68fb..48ad161 100644 --- a/src/login/KcPage.tsx +++ b/src/login/KcPage.tsx @@ -1,3 +1,4 @@ +import "./main.css"; import { Suspense, lazy } from "react"; import type { ClassKey } from "keycloakify/login"; import type { KcContext } from "./KcContext"; diff --git a/src/login/main.css b/src/login/main.css new file mode 100644 index 0000000..5854c73 --- /dev/null +++ b/src/login/main.css @@ -0,0 +1,147 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap'); + +@font-face { + font-family: "Hermit"; + src: url(https://penguinowl.dev/assets/Hermit-Regular.otf) format("opentype"); +} + +html { + background: #010b0f; +} + +body.kcBodyClass { + background: linear-gradient(150deg, rgb(1, 11, 15) 0%, rgb(33, 33, 38) 100%); +} + +div.kcHeaderClass { + color: #f0f0f0 !important; +} + +h1#kc-page-title { + font-family: "Lato"; + font-weight: 700; + font-style: normal; + text-align: left; + color: #f0f0f0; +} + +label.kcLabelClass { + color: #f0f0f0; + font-family: "Lato"; + font-weight: 700; +} + +header.kcFormHeaderClass { + flex-direction: row-reverse !important; + justify-content: space-between; + align-items: center; +} + +input.kcButtonClass { + font-family: "Hermit"; + font-size: 18px; + background-color: green !important; + border-radius: 2px; +} + +#kc-current-locale-link { + color: #e0e0e0 !important; +} + +button.kcFormPasswordVisibilityButtonClass { + background-color: #333333 !important; + border-color: yellow !important; + height: 36px; + border-width: 1px !important; + border-radius: 2px !important; + border-style: solid; +} + +i.kcFormPasswordVisibilityIconShow { + color: #f0f0f0 !important; +} + +i.kcFormPasswordVisibilityIconHide { + color: #f0f0f0 !important; +} + +#kc-header { + color: inherit; + margin-bottom: 0px; +} + +div.kcFormCardClass { + border-radius: 30px; + border-color: none; + border-top: none; + box-shadow: 0px 0px 40px #53504a70; + border-style: solid; + border-color: #9b9b19; + background: #05061c; +} + +input.kcInputClass { + background-color: #333333; + border-color: yellow; + border-radius: 2px; + color: #f0f0f0; +} + +@media (max-width: 767px) { + #kc-header-wrapper { + color: #f0f0f0; + margin-bottom: 13px; + margin-left: 18px; + } + #kc-locale { + top: 36px; + } + #kc-page-title { + margin-bottom: -10px; + } + header.kcFormHeaderClass { + margin-top: 10px; + justify-content: left; + } + .login-pf-page .card-pf { + max-width: none; + margin-left: 10px; + margin-right: 10px; + padding-top: 2px; + border-top-width: 2px; + border-top-color: rgb(155, 155, 25) !important; + border-top: solid; + box-shadow: 0 0; + } +} + +#kc-form-buttons { + margin-top: 0px; +} + +a { + color: #e0e000; +} + +.login-pf a:hover { + color: #a0f000; +} + +#kc-form-options .checkbox { + color: #f0f0f0 +} + +.pf-m-control::after { + border-width: 0; + border-bottom-width: 0 !important; +} + +.pf-c-button.pf-m-control:hover::after { + border-bottom-width: 0; +} + +div.kcInputGroup { + border-radius: 2px; + border-width: 0px; + background-color: yellow; +} diff --git a/src/login/pages/Login.stories.tsx b/src/login/pages/Login.stories.tsx new file mode 100644 index 0000000..adadf0a --- /dev/null +++ b/src/login/pages/Login.stories.tsx @@ -0,0 +1,360 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { createKcPageStory } from "../KcPageStory"; + +const { KcPageStory } = createKcPageStory({ pageId: "login.ftl" }); + +const meta = { + title: "login/login.ftl", + component: KcPageStory +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; + +export const WithInvalidCredential: Story = { + render: () => ( + { + const fieldNames = [fieldName, ...otherFieldNames]; + return fieldNames.includes("username") || fieldNames.includes("password"); + }, + get: (fieldName: string) => { + if (fieldName === "username" || fieldName === "password") { + return "Invalid username or password."; + } + return ""; + } + } + }} + /> + ) +}; + +export const WithoutRegistration: Story = { + render: () => ( + + ) +}; + +export const WithoutRememberMe: Story = { + render: () => ( + + ) +}; + +export const WithoutPasswordReset: Story = { + render: () => ( + + ) +}; + +export const WithEmailAsUsername: Story = { + render: () => ( + + ) +}; + +export const WithPresetUsername: Story = { + render: () => ( + + ) +}; + +export const WithImmutablePresetUsername: Story = { + render: () => ( + + ) +}; + +export const WithSocialProviders: Story = { + render: () => ( + + ) +}; + +export const WithoutPasswordField: Story = { + render: () => ( + + ) +}; + +export const WithErrorMessage: Story = { + render: () => ( + The login process will restart from the beginning.", + type: "error" + } + }} + /> + ) +}; + +export const WithOneSocialProvider: Story = { + render: args => ( + + ) +}; + +export const WithTwoSocialProviders: Story = { + render: args => ( + + ) +}; +export const WithNoSocialProviders: Story = { + render: args => ( + + ) +}; +export const WithMoreThanTwoSocialProviders: Story = { + render: args => ( + + ) +}; +export const WithSocialProvidersAndWithoutRememberMe: Story = { + render: args => ( + + ) +}; diff --git a/src/login/pages/Login.tsx b/src/login/pages/Login.tsx new file mode 100644 index 0000000..14c40e0 --- /dev/null +++ b/src/login/pages/Login.tsx @@ -0,0 +1,219 @@ +import type { JSX } from "keycloakify/tools/JSX"; +import { useState } from "react"; +import { kcSanitize } from "keycloakify/lib/kcSanitize"; +import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed"; +import { clsx } from "keycloakify/tools/clsx"; +import type { PageProps } from "keycloakify/login/pages/PageProps"; +import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; +import type { KcContext } from "../KcContext"; +import type { I18n } from "../i18n"; + +export default function Login(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; + + const { kcClsx } = getKcClsx({ + doUseDefaultCss, + classes + }); + + const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext; + + const { msg, msgStr } = i18n; + + const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); + + return ( + + ); +} + +function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: string; children: JSX.Element }) { + const { kcClsx, i18n, passwordInputId, children } = props; + + const { msgStr } = i18n; + + const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId }); + + return ( +
+ {children} + +
+ ); +}