showpane 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/bundle/meta/scaffold-manifest.json +73 -0
- package/bundle/scaffold/VERSION +1 -0
- package/bundle/scaffold/__dot__env.example +24 -0
- package/bundle/scaffold/__dot__gitignore +41 -0
- package/bundle/scaffold/docker/Caddyfile +3 -0
- package/bundle/scaffold/docker/Dockerfile +30 -0
- package/bundle/scaffold/docker-compose.yml +53 -0
- package/bundle/scaffold/next.config.ts +20 -0
- package/bundle/scaffold/package-lock.json +5843 -0
- package/bundle/scaffold/package.json +42 -0
- package/bundle/scaffold/postcss.config.js +6 -0
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
- package/bundle/scaffold/prisma/schema.local.prisma +131 -0
- package/bundle/scaffold/prisma/schema.prisma +128 -0
- package/bundle/scaffold/prisma/seed.ts +49 -0
- package/bundle/scaffold/public/example-avatar.svg +4 -0
- package/bundle/scaffold/public/example-logo.svg +4 -0
- package/bundle/scaffold/public/robots.txt +2 -0
- package/bundle/scaffold/scripts/backup.sh +19 -0
- package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
- package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
- package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
- package/bundle/scaffold/scripts/restore.sh +31 -0
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
- package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
- package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
- package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
- package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
- package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
- package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
- package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
- package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
- package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
- package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
- package/bundle/scaffold/src/app/api/health/route.ts +19 -0
- package/bundle/scaffold/src/app/globals.css +7 -0
- package/bundle/scaffold/src/app/layout.tsx +25 -0
- package/bundle/scaffold/src/app/page.tsx +171 -0
- package/bundle/scaffold/src/components/portal-login.tsx +169 -0
- package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
- package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
- package/bundle/scaffold/src/lib/branding.ts +50 -0
- package/bundle/scaffold/src/lib/client-auth.ts +98 -0
- package/bundle/scaffold/src/lib/client-portals.ts +134 -0
- package/bundle/scaffold/src/lib/control-plane.ts +100 -0
- package/bundle/scaffold/src/lib/db.ts +7 -0
- package/bundle/scaffold/src/lib/files.ts +124 -0
- package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
- package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
- package/bundle/scaffold/src/lib/storage.ts +204 -0
- package/bundle/scaffold/src/lib/token.ts +186 -0
- package/bundle/scaffold/src/lib/utils.ts +6 -0
- package/bundle/scaffold/src/middleware.ts +61 -0
- package/bundle/scaffold/tailwind.config.ts +15 -0
- package/bundle/scaffold/tests/__dot__gitkeep +0 -0
- package/bundle/scaffold/tsconfig.json +23 -0
- package/bundle/scaffold/vitest.config.ts +13 -0
- package/bundle/toolchain/VERSION +1 -0
- package/bundle/toolchain/bin/check-slug.ts +59 -0
- package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
- package/bundle/toolchain/bin/create-portal.ts +71 -0
- package/bundle/toolchain/bin/delete-portal.ts +48 -0
- package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
- package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
- package/bundle/toolchain/bin/generate-share-link.ts +68 -0
- package/bundle/toolchain/bin/list-portals.ts +53 -0
- package/bundle/toolchain/bin/materialize-file.ts +35 -0
- package/bundle/toolchain/bin/query-analytics.ts +88 -0
- package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
- package/bundle/toolchain/bin/showpane-config +63 -0
- package/bundle/toolchain/bin/tsconfig.json +13 -0
- package/bundle/toolchain/skills/VERSION +1 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
- package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
- package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
- package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
- package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
- package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
- package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
- package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
- package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
- package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
- package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
- package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
- package/bundle/toolchain/skills/shared/preamble.md +137 -0
- package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
- package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
- package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
- package/dist/index.js +873 -166
- package/package.json +3 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { prisma } from "@/lib/db";
|
|
2
|
+
import { getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { Presentation, Briefcase, UserPlus } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
const templates = [
|
|
7
|
+
{
|
|
8
|
+
name: "Sales Follow-up",
|
|
9
|
+
description: "Meeting notes, next steps, documents",
|
|
10
|
+
icon: Presentation,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "Consulting",
|
|
14
|
+
description: "Project overview, deliverables, timeline",
|
|
15
|
+
icon: Briefcase,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "Onboarding",
|
|
19
|
+
description: "Welcome, setup steps, resources",
|
|
20
|
+
icon: UserPlus,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export default async function Home() {
|
|
25
|
+
let portalCount = 0;
|
|
26
|
+
try {
|
|
27
|
+
if (isRuntimeSnapshotMode()) {
|
|
28
|
+
const state = await getRuntimeState();
|
|
29
|
+
portalCount = state?.portals.length ?? 0;
|
|
30
|
+
} else {
|
|
31
|
+
portalCount = await prisma.clientPortal.count();
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// DB not ready yet — show welcome page with 0 portals
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<main className="min-h-screen flex flex-col">
|
|
39
|
+
{/* Hero zone */}
|
|
40
|
+
<div className="bg-gradient-to-b from-[#2C5278] to-[#5A8BB5] px-4 py-16 md:py-24 text-center relative overflow-hidden">
|
|
41
|
+
<div
|
|
42
|
+
className="absolute inset-0 opacity-[0.07]"
|
|
43
|
+
style={{
|
|
44
|
+
backgroundImage: "radial-gradient(circle, white 1px, transparent 1px)",
|
|
45
|
+
backgroundSize: "24px 24px",
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
<div className="relative">
|
|
49
|
+
<h1 className="sr-only">SHOWPANE</h1>
|
|
50
|
+
<div
|
|
51
|
+
role="img"
|
|
52
|
+
aria-label="SHOWPANE"
|
|
53
|
+
className="font-mono text-white text-[0.45rem] leading-[1.1] sm:text-[0.55rem] md:text-xs whitespace-pre select-none mx-auto w-fit"
|
|
54
|
+
>
|
|
55
|
+
{`███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ █████╗ ███╗ ██╗███████╗
|
|
56
|
+
██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗██╔══██╗████╗ ██║██╔════╝
|
|
57
|
+
███████╗███████║██║ ██║██║ █╗ ██║██████╔╝███████║██╔██╗ ██║█████╗
|
|
58
|
+
╚════██║██╔══██║██║ ██║██║███╗██║██╔═══╝ ██╔══██║██║╚██╗██║██╔══╝
|
|
59
|
+
███████║██║ ██║╚██████╔╝╚███╔███╔╝██║ ██║ ██║██║ ╚████║███████╗
|
|
60
|
+
╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝`}
|
|
61
|
+
</div>
|
|
62
|
+
<p className="mt-6 text-white/90 text-lg">
|
|
63
|
+
Create professional client portals with Claude Code.
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Action zone */}
|
|
69
|
+
<div className="flex-1 bg-[#FDFBF7] px-4 py-12 md:py-16">
|
|
70
|
+
<div className="max-w-lg mx-auto">
|
|
71
|
+
{/* Steps */}
|
|
72
|
+
<ol className="space-y-4">
|
|
73
|
+
<li className="border border-gray-200 rounded-lg p-5 bg-white">
|
|
74
|
+
<div className="flex items-start gap-4">
|
|
75
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
|
|
76
|
+
1
|
|
77
|
+
</span>
|
|
78
|
+
<div className="min-w-0">
|
|
79
|
+
<p className="text-gray-900 font-medium mb-2">
|
|
80
|
+
Open your terminal in this directory
|
|
81
|
+
</p>
|
|
82
|
+
<code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
|
|
83
|
+
cd app
|
|
84
|
+
</code>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</li>
|
|
88
|
+
|
|
89
|
+
<li className="border border-gray-200 rounded-lg p-5 bg-white">
|
|
90
|
+
<div className="flex items-start gap-4">
|
|
91
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
|
|
92
|
+
2
|
|
93
|
+
</span>
|
|
94
|
+
<div className="min-w-0">
|
|
95
|
+
<p className="text-gray-900 font-medium mb-2">
|
|
96
|
+
Launch Claude Code
|
|
97
|
+
</p>
|
|
98
|
+
<code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded">
|
|
99
|
+
claude
|
|
100
|
+
</code>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</li>
|
|
104
|
+
|
|
105
|
+
<li className="border border-gray-200 rounded-lg p-5 bg-white">
|
|
106
|
+
<div className="flex items-start gap-4">
|
|
107
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
|
|
108
|
+
3
|
|
109
|
+
</span>
|
|
110
|
+
<div className="min-w-0">
|
|
111
|
+
<p className="text-gray-900 font-medium mb-2">
|
|
112
|
+
Tell it what to create
|
|
113
|
+
</p>
|
|
114
|
+
<code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
|
|
115
|
+
Create a portal for my call with [client name]
|
|
116
|
+
</code>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</li>
|
|
120
|
+
</ol>
|
|
121
|
+
|
|
122
|
+
<p className="mt-4 text-xs text-gray-400 text-center">
|
|
123
|
+
Don't have Claude Code?{" "}
|
|
124
|
+
<a
|
|
125
|
+
href="https://claude.ai/code"
|
|
126
|
+
className="text-blue-600 hover:underline"
|
|
127
|
+
target="_blank"
|
|
128
|
+
rel="noopener noreferrer"
|
|
129
|
+
>
|
|
130
|
+
Install it here
|
|
131
|
+
</a>
|
|
132
|
+
</p>
|
|
133
|
+
|
|
134
|
+
{/* Template previews */}
|
|
135
|
+
<div className="mt-12">
|
|
136
|
+
<p className="text-sm font-medium text-gray-500 text-center mb-4">
|
|
137
|
+
Claude Code generates portals from templates
|
|
138
|
+
</p>
|
|
139
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
140
|
+
{templates.map((t) => (
|
|
141
|
+
<div
|
|
142
|
+
key={t.name}
|
|
143
|
+
className="border border-gray-200 rounded-lg p-4 bg-white text-center"
|
|
144
|
+
>
|
|
145
|
+
<t.icon className="h-5 w-5 text-gray-400 mx-auto mb-2" />
|
|
146
|
+
<p className="text-sm font-medium text-gray-900">{t.name}</p>
|
|
147
|
+
<p className="text-xs text-gray-500 mt-1">{t.description}</p>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Footer zone */}
|
|
156
|
+
<footer className="bg-[#FDFBF7] px-4 pb-8 text-center space-y-2">
|
|
157
|
+
{portalCount > 0 && (
|
|
158
|
+
<p className="text-sm text-gray-500">
|
|
159
|
+
You have {portalCount} portal{portalCount !== 1 ? "s" : ""}.{" "}
|
|
160
|
+
<Link href="/client" className="text-blue-600 hover:underline">
|
|
161
|
+
Go to login
|
|
162
|
+
</Link>
|
|
163
|
+
</p>
|
|
164
|
+
)}
|
|
165
|
+
<p className="text-xs text-gray-400">
|
|
166
|
+
Powered by Claude Code
|
|
167
|
+
</p>
|
|
168
|
+
</footer>
|
|
169
|
+
</main>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, FormEvent, ReactNode } from "react";
|
|
4
|
+
import { ArrowRight, Eye, EyeOff } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export type PortalLoginProps = {
|
|
7
|
+
companyName: string;
|
|
8
|
+
companyLogo: ReactNode;
|
|
9
|
+
companyUrl: string;
|
|
10
|
+
portalLabel?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
supportEmail: string;
|
|
13
|
+
authEndpoint?: string;
|
|
14
|
+
redirectBasePath?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function PortalLogin({
|
|
18
|
+
companyName,
|
|
19
|
+
companyLogo,
|
|
20
|
+
companyUrl,
|
|
21
|
+
portalLabel,
|
|
22
|
+
description,
|
|
23
|
+
supportEmail,
|
|
24
|
+
authEndpoint,
|
|
25
|
+
redirectBasePath,
|
|
26
|
+
}: PortalLoginProps) {
|
|
27
|
+
const [username, setUsername] = useState("");
|
|
28
|
+
const [password, setPassword] = useState("");
|
|
29
|
+
const [error, setError] = useState<{ message: string; hint?: string } | null>(null);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
32
|
+
|
|
33
|
+
const resolvedPortalLabel = portalLabel ?? "Client Portal";
|
|
34
|
+
const resolvedDescription = description ?? `Private portal for ${companyName} clients. Sign in with the credentials we sent you.`;
|
|
35
|
+
const resolvedAuthEndpoint = authEndpoint ?? "/api/client-auth";
|
|
36
|
+
const resolvedRedirectBasePath = redirectBasePath ?? "/client";
|
|
37
|
+
|
|
38
|
+
let displayDomain: string;
|
|
39
|
+
try {
|
|
40
|
+
displayDomain = new URL(companyUrl).hostname;
|
|
41
|
+
} catch {
|
|
42
|
+
displayDomain = companyUrl;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleSubmit(e: FormEvent) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
if (loading) return;
|
|
48
|
+
setLoading(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(resolvedAuthEndpoint, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ username: username.trim(), password }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (res.ok) {
|
|
59
|
+
const { slug } = await res.json();
|
|
60
|
+
window.location.href = `${resolvedRedirectBasePath}/${slug}`;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (res.status === 429) {
|
|
65
|
+
setError({ message: "Too many attempts", hint: "Please wait a minute before trying again." });
|
|
66
|
+
} else if (res.status === 503) {
|
|
67
|
+
setError({ message: "Portal auth is not configured", hint: "Restart the dev server after updating environment variables." });
|
|
68
|
+
} else if (res.status >= 500) {
|
|
69
|
+
setError({ message: "Unable to sign in", hint: "The server returned an error. Try again in a moment." });
|
|
70
|
+
} else {
|
|
71
|
+
setError({ message: "Invalid username or password", hint: "Credentials are case-sensitive. Check for typos." });
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
setError({ message: "Unable to connect", hint: "Check your internet connection and try again." });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const canSubmit = username.trim().length > 0 && password.length > 0 && !loading;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gray-100 px-4">
|
|
84
|
+
<div className="absolute -right-32 -top-32 h-96 w-96 rounded-full bg-primary/10 blur-[140px]" />
|
|
85
|
+
<div className="absolute -bottom-32 -left-32 h-96 w-96 rounded-full bg-primary/5 blur-[120px]" />
|
|
86
|
+
|
|
87
|
+
<div className="relative z-10 w-full max-w-sm">
|
|
88
|
+
<a href={companyUrl} className="mx-auto mb-8 flex w-fit items-center gap-2 transition-opacity hover:opacity-70">
|
|
89
|
+
{companyLogo}
|
|
90
|
+
<span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
|
|
91
|
+
</a>
|
|
92
|
+
|
|
93
|
+
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
94
|
+
<div className="mb-5">
|
|
95
|
+
<h1 className="text-xl font-bold tracking-tight text-gray-900">{resolvedPortalLabel}</h1>
|
|
96
|
+
<p className="mt-1 text-sm leading-relaxed text-gray-500">
|
|
97
|
+
{resolvedDescription}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
102
|
+
<div>
|
|
103
|
+
<label htmlFor="username" className="block text-sm font-medium text-gray-700">Company Name</label>
|
|
104
|
+
<input
|
|
105
|
+
id="username"
|
|
106
|
+
type="text"
|
|
107
|
+
autoFocus
|
|
108
|
+
autoComplete="username"
|
|
109
|
+
autoCapitalize="off"
|
|
110
|
+
autoCorrect="off"
|
|
111
|
+
spellCheck={false}
|
|
112
|
+
value={username}
|
|
113
|
+
onChange={(e) => { setUsername(e.target.value); setError(null); }}
|
|
114
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
|
|
115
|
+
required
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div>
|
|
119
|
+
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
|
120
|
+
<div className="relative mt-1">
|
|
121
|
+
<input
|
|
122
|
+
id="password"
|
|
123
|
+
type={showPassword ? "text" : "password"}
|
|
124
|
+
autoComplete="current-password"
|
|
125
|
+
value={password}
|
|
126
|
+
onChange={(e) => { setPassword(e.target.value); setError(null); }}
|
|
127
|
+
className="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 pr-10 text-sm text-gray-900 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
|
|
128
|
+
required
|
|
129
|
+
/>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
133
|
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 cursor-pointer text-gray-400 transition-colors hover:text-gray-600"
|
|
134
|
+
tabIndex={-1}
|
|
135
|
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
136
|
+
>
|
|
137
|
+
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{error && (
|
|
142
|
+
<div className="rounded-lg bg-red-50 px-3 py-2">
|
|
143
|
+
<p className="text-sm text-red-600">{error.message}</p>
|
|
144
|
+
{error.hint && <p className="mt-0.5 text-xs text-red-500/70">{error.hint}</p>}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
<button
|
|
148
|
+
type="submit"
|
|
149
|
+
disabled={!canSubmit}
|
|
150
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-gray-800 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40"
|
|
151
|
+
>
|
|
152
|
+
{loading ? "Signing in..." : "Sign In"}
|
|
153
|
+
{!loading && <ArrowRight className="h-4 w-4" />}
|
|
154
|
+
</button>
|
|
155
|
+
</form>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="mt-5 space-y-2 text-center text-xs">
|
|
159
|
+
<p className="text-gray-400">
|
|
160
|
+
Not a client? <a href={companyUrl} className="text-gray-500 underline underline-offset-2 transition-colors hover:text-gray-700">Visit {displayDomain}</a>
|
|
161
|
+
</p>
|
|
162
|
+
<p className="text-gray-400">
|
|
163
|
+
Lost your credentials? Email <span className="font-medium text-gray-500">{supportEmail}</span>
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|