diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..b66c933 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,91 @@ +import styled from "styled-components"; + +interface Props { + variant?: "primary" | "secondary" | "danger" | "success" | "warning"; + outlined?: boolean; +} + +export default styled.button` +background: ${(props) => { + if (props.outlined) return "transparent"; + switch (props.variant) { + case "primary": + return "var(--button-primary)"; + case "secondary": + return "var(--button-secondary)"; + case "danger": + return "var(--button-danger)"; + case "success": + return "var(--button-success)"; + case "warning": + return "var(--button-warning)"; + default: + return "var(--button-primary)"; + } +}}; + +border: ${(props) => { + if (!props.outlined) return "none"; + switch (props.variant) { + case "primary": + return "1px solid var(--button-primary)"; + case "secondary": + return "1px solid var(--button-secondary)"; + case "danger": + return "1px solid var(--button-danger)"; + case "success": + return "1px solid var(--button-success)"; + case "warning": + return "1px solid var(--button-warning)"; + default: + return "1px solid var(--button-primary)"; + } +}}; + +color: var(--text); +padding: 8px 16px; +border-radius: 8px; +font-size: 13px; +font-weight: 700; +cursor: pointer; +outline: none; +transition: background 0.2s ease-in-out; +pointer-events:${(props) => (props.disabled ? "none" : null)}; +opacity: ${(props) => (props.disabled ? 0.5 : 1)}; + +&:hover { + background: ${(props) => { + switch (props.variant) { + case "primary": + return "var(--button-primary-hover)"; + case "secondary": + return "var(--button-secondary-hover)"; + case "danger": + return "var(--button-danger-hover)"; + case "success": + return "var(--button-success-hover)"; + case "warning": + return "var(--button-warning-hover)"; + default: + return "var(--button-primary-hover)"; + } + }}; + +&:active { + background: ${(props) => { + switch (props.variant) { + case "primary": + return "var(--button-primary-active)"; + case "secondary": + return "var(--button-secondary-active)"; + case "danger": + return "var(--button-danger-active)"; + case "success": + return "var(--button-success-active)"; + case "warning": + return "var(--button-warning-active)"; + default: + return "var(--button-primary-active)"; + } + }}; +`; diff --git a/src/contexts/Theme.tsx b/src/contexts/Theme.tsx index 245d676..171b58b 100644 --- a/src/contexts/Theme.tsx +++ b/src/contexts/Theme.tsx @@ -11,7 +11,23 @@ export type ThemeVariables = | "tertiary" | "text" | "textMuted" - | "inputBackground"; + | "inputBackground" + | "error" + | "buttonPrimary" + | "buttonPrimaryHover" + | "buttonPrimaryActive" + | "buttonSecondary" + | "buttonSecondaryHover" + | "buttonSecondaryActive" + | "buttonDanger" + | "buttonDangerHover" + | "buttonDangerActive" + | "buttonSuccess" + | "buttonSuccessHover" + | "buttonSuccessActive" + | "buttonWarning" + | "buttonWarningHover" + | "buttonWarningActive"; export type Overrides = { [variable in ThemeVariables]: string; @@ -32,6 +48,22 @@ export const ThemePresets: Record = { text: "#000000", textMuted: "#232120", inputBackground: "#757575", + error: "#e83f36", + buttonPrimary: "", + buttonPrimaryHover: "", + buttonPrimaryActive: "", + buttonSecondary: "", + buttonSecondaryHover: "", + buttonSecondaryActive: "", + buttonDanger: "", + buttonDangerHover: "", + buttonDangerActive: "", + buttonSuccess: "", + buttonSuccessHover: "", + buttonSuccessActive: "", + buttonWarning: "", + buttonWarningHover: "", + buttonWarningActive: "", }, dark: { brandPrimary: "#FF5F00", @@ -43,6 +75,23 @@ export const ThemePresets: Record = { text: "#e9e2e1", textMuted: "#85898f", inputBackground: "#121212", + error: "#e83f36", + // buttons + buttonPrimary: "#FF5F00", + buttonPrimaryHover: "#ff3d00", + buttonPrimaryActive: "#BA4500", + buttonSecondary: "#4a4544", + buttonSecondaryHover: "#746d69", + buttonSecondaryActive: "#5f5a59", + buttonDanger: "#ff3a3b", + buttonDangerHover: "#ff2d2f", + buttonDangerActive: "#ff2425", + buttonSuccess: "#34af65", + buttonSuccessHover: "#31a660", + buttonSuccessActive: "#2d9657", + buttonWarning: "#faa61a", + buttonWarningHover: "#e69105", + buttonWarningActive: "#c27b04", }, }; @@ -52,6 +101,9 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>` } `; +const toDashed = (str: string) => + str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()); + export const generateVariables = (theme: Theme) => { return (Object.keys(theme) as ThemeVariables[]).map((key) => { const colour = theme[key]; @@ -59,9 +111,11 @@ export const generateVariables = (theme: Theme) => { const r = parseInt(colour.substring(1, 3), 16); const g = parseInt(colour.substring(3, 5), 16); const b = parseInt(colour.substring(5, 7), 16); - return `--${key}: ${theme[key]}; --${key}-rgb: rgb(${r}, ${g}, ${b});`; + return `--${toDashed(key)}: ${theme[key]}; --${toDashed( + key + )}-rgb: rgb(${r}, ${g}, ${b});`; } catch { - return `--${key}: ${theme[key]};`; + return `--${toDashed(key)}: ${theme[key]};`; } }); }; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 33d2950..7d0d449 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,5 +1,6 @@ import { useForm } from "react-hook-form"; import styled from "styled-components"; +import Button from "../components/Button"; import Container from "../components/Container"; const Wrapper = styled(Container)` @@ -11,10 +12,10 @@ const Wrapper = styled(Container)` `; const LoginBox = styled(Container)` - background-color: var(--primaryAlt); + background-color: var(--primary-alt); padding: 32px; font-size: 18px; - color: var(--textMuted); + color: var(--text-muted); display: flex; flex-direction: column; align-items: center; @@ -43,7 +44,7 @@ const Header = styled.h1` `; const SubHeader = styled.h2` - color: var(--textMuted); + color: var(--text-muted); font-weight: 400; font-size: 16px; `; @@ -59,9 +60,20 @@ const InputContainer = styled.h1<{ marginBottom: boolean }>` align-items: flex-start; `; -const InputLabel = styled.label` - color: #b1b5bc; +const LabelWrapper = styled.div<{ error?: boolean }>` + display: flex; + flex-direction: row; margin-bottom: 8px; + color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")}; +`; + +const InputErrorText = styled.label` + font-size: 14px; + font-weight: 400; + font-style: italic; +`; + +const InputLabel = styled.label` font-size: 14px; font-weight: 700; `; @@ -71,7 +83,7 @@ const InputWrapper = styled.div` display: flex; `; -const Input = styled.input<{invalid?: boolean}>` +const Input = styled.input<{ error?: boolean }>` outline: none; background: var(--secondary); padding: 10px; @@ -80,8 +92,8 @@ const Input = styled.input<{invalid?: boolean}>` border-radius: 12px; color: var(--text); margin: 0; - border: ${(props) => props.invalid ? "1px solid red" : "none"}; - aria-invalid: ${(props) => props.invalid ? "true" : "false"}; + border: none; + aria-invalid: ${(props) => (props.error ? "true" : "false")}; `; const PasswordResetLink = styled.a` @@ -93,16 +105,11 @@ const PasswordResetLink = styled.a` text-decoration: none; `; -const Button = styled.button` - background: var(--brandPrimary); - color: var(--text); - font-size: 16px; +const LoginButton = styled(Button)` margin-bottom: 8px; width: 100%; min-width: 130px; min-height: 44px; - border-radius: 8px; - border: none; `; const RegisterContainer = styled.div` @@ -123,11 +130,18 @@ const RegisterLink = styled.a` } `; -function LoginPage() { - const { register, handleSubmit, watch, formState: { errors } } = useForm(); - const onSubmit = (data: any) => console.log(data); +const Divider = styled.span` + padding: 0 4px; +`; - console.log(errors) +function LoginPage() { + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm(); + const onSubmit = (data: any) => console.log(data); return ( @@ -139,21 +153,46 @@ function LoginPage() { - Email + + Email + {errors.email && ( + + -Email error here + + )} + - + - Password + + Password + {errors.password && ( + + -Password error here + + )} + - + Forgot your password? - + + Log In + Don't have an account?