Blocks · @jengacore/sign-in
Sign-in
The shared sign-in: navy brand panel with the four products, environment choice, email and password, single sign-on.
Digital public infrastructure
Building digital public infrastructure for an inclusive future.
Monitoring
Core banking
Finance
Disputes
Sign in
Settlement, positions and participant finance for the hub.
or
Access is logged and reviewed. Ask your administrator for an account.
v2.4.0 · PROD · jengacore
Install
terminal
npx shadcn@latest add @jengacore/sign-inWithout the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/sign-in.json
Source
components/blocks/sign-in.tsx
"use client"
import * as React from "react"
import { Info, Lock, LogIn } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { FormField } from "@/components/jc/form-field"
import { Segmented } from "@/components/jc/segmented"
import { PRODUCT_NAME, PRODUCT_TILE, type Product } from "@/components/jc/theme"
import { cn } from "@/lib/utils"
const PRODUCTS: { key: Product; label: string; file: string }[] = [
{ key: "mulisa", label: "Monitoring", file: "mulisa" },
{ key: "msingi", label: "Core banking", file: "msingi" },
{ key: "finance", label: "Finance", file: "finance-portal" },
{ key: "disputes", label: "Disputes", file: "disputes" },
]
const ENV_LABEL = { prod: "Production", uat: "UAT", sandbox: "Sandbox", dev: "Dev" } as const
type Env = keyof typeof ENV_LABEL
/**
* The shared sign-in: the brand panel on navy (diagonal hairlines, the jengacore. wordmark, the four products
* with the current one marked) and the form on white. Wire `onSubmit` to the app's credentials provider and
* `onSso` to its OIDC provider; leave either out to hide it.
*/
export function SignIn({
product,
description,
environments,
defaultEnvironment,
onSubmit,
onSso,
ssoLabel = "Continue with single sign-on",
forgotHref,
error,
notice,
version,
}: {
product: Product
/** One sentence: what this product is for. */
description: string
environments?: Env[]
defaultEnvironment?: Env
onSubmit?: (values: { email: string; password: string; environment?: Env }) => void | Promise<void>
/** May return a promise: the button shows "Redirecting…" and stays disabled until it settles. */
onSso?: () => void | Promise<void>
ssoLabel?: string
/** Informational line above the form ("Invitation accepted. Sign in to continue."). Errors go in `error`. */
notice?: string
forgotHref?: string
/** Shown under the form: say how to fix it ("Check the email and password, then try again."). */
error?: string
/** Footer line, e.g. "v2.4.0 · PROD". */
version?: string
}) {
const [env, setEnv] = React.useState<Env | undefined>(defaultEnvironment ?? environments?.[0])
const [pending, setPending] = React.useState(false)
const [ssoPending, setSsoPending] = React.useState(false)
async function sso() {
if (!onSso) return
setSsoPending(true)
try {
await onSso()
} finally {
setSsoPending(false)
}
}
const file = PRODUCTS.find((p) => p.key === product)!.file
async function submit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!onSubmit) return
const data = new FormData(e.currentTarget)
setPending(true)
try {
await onSubmit({ email: String(data.get("email") ?? ""), password: String(data.get("password") ?? ""), environment: env })
} finally {
setPending(false)
}
}
return (
<div data-slot="sign-in" className="grid min-h-svh lg:grid-cols-[minmax(0,760px)_1fr]">
<section className="relative flex flex-col justify-between overflow-hidden bg-navy-950 px-14 py-12 text-white max-lg:hidden">
<svg className="pointer-events-none absolute inset-0 h-full w-full" aria-hidden preserveAspectRatio="none" viewBox="0 0 760 900">
{[0, 1, 2, 3, 4].map((i) => (
<line key={i} x1={340 + i * 90} y1={0} x2={760} y2={420 - i * 90 * (420 / 420)} stroke="white" strokeOpacity={0.55} strokeWidth={0.8} />
))}
</svg>
<img src="/brand/jengacore-wordmark-rule-white.svg" alt="jengacore." className="relative h-10 w-auto self-start" />
<div className="relative max-w-[520px]">
<div className="font-mono text-[11px] tracking-[0.16em] text-emerald-400 uppercase">Digital public infrastructure</div>
<p className="mt-4 text-[52px] leading-[1.02] font-extrabold tracking-[-0.01em]">
Building digital public infrastructure for an <span className="text-emerald-400">inclusive</span> future.
</p>
</div>
<ul className="relative flex flex-wrap gap-3" aria-label="jengacore products">
{PRODUCTS.map((p) => (
<li
key={p.key}
aria-current={p.key === product ? "true" : undefined}
className={cn(
"flex items-center gap-2.5 rounded-md border px-2.5 py-2 text-[13px] text-grey-200",
p.key === product ? "border-emerald-400/70 bg-white/[0.04]" : "border-white/10"
)}
>
<img src={PRODUCT_TILE[p.key]} alt="" className="size-6 rounded-xs" />
{p.label}
</li>
))}
</ul>
</section>
<section className="flex items-center justify-center bg-background px-6 py-12">
<div className="w-full max-w-[380px]">
<img src={`/brand/${file}-wordmark.svg`} alt={PRODUCT_NAME[product]} className="h-7 w-auto dark:hidden" />
<img src={`/brand/${file}-wordmark-white.svg`} alt={PRODUCT_NAME[product]} className="hidden h-7 w-auto dark:block" />
<h1 className="mt-10 text-[26px] leading-[34px] font-semibold">Sign in</h1>
<p className="mt-1 text-[13.5px] text-muted-foreground">{description}</p>
{notice ? (
<p role="status" className="mt-5 flex items-start gap-2 rounded-md bg-info-subtle px-3 py-2.5 text-[13px] text-foreground">
<Info className="mt-px size-4 shrink-0 stroke-[1.75] text-info" aria-hidden />
{notice}
</p>
) : null}
<form className="mt-6 grid gap-4" onSubmit={submit}>
{environments?.length && env ? (
<div className="grid gap-1.5">
<span className="text-xs font-medium" id="jc-env-label">
Environment
</span>
<Segmented
label="Environment"
value={env}
onValueChange={setEnv}
size="lg"
className="w-full"
options={environments.map((e) => ({ value: e, label: ENV_LABEL[e] }))}
/>
</div>
) : null}
{onSubmit ? (
<>
<FormField label="Work email" htmlFor="jc-email">
<Input id="jc-email" name="email" type="email" autoComplete="username" required />
</FormField>
<div className="grid gap-1.5">
<div className="flex items-baseline justify-between">
<label htmlFor="jc-password" className="text-xs font-medium">
Password
</label>
{forgotHref ? (
<a href={forgotHref} className="text-xs text-primary hover:underline">
Forgot?
</a>
) : null}
</div>
<Input id="jc-password" name="password" type="password" autoComplete="current-password" required aria-invalid={error ? true : undefined} />
</div>
<Button type="submit" size="lg" disabled={pending} className="w-full">
<LogIn />
{pending ? "Signing in…" : env ? `Sign in to ${ENV_LABEL[env]}` : "Sign in"}
</Button>
</>
) : null}
{error ? (
<p role="alert" className="text-[13px] text-destructive">
{error}
</p>
) : null}
</form>
{onSso ? (
<>
{onSubmit ? (
<div className="my-6 flex items-center gap-3 font-mono text-[10.5px] tracking-[0.14em] text-muted-foreground uppercase before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">
or
</div>
) : (
<div className="mt-6" />
)}
<Button type="button" variant={onSubmit ? "outline" : "default"} size="lg" className="w-full" onClick={sso} loading={ssoPending}>
{ssoPending ? null : <Lock />}
{ssoPending ? "Redirecting…" : ssoLabel}
</Button>
</>
) : null}
<p className="mt-6 text-[12.5px] text-muted-foreground">Access is logged and reviewed. Ask your administrator for an account.</p>
{version ? <p className="mt-4 font-mono text-[11px] tracking-[0.08em] text-muted-foreground">{version} · jengacore</p> : null}
</div>
</section>
</div>
)
}