Skip to content

Commit 4fab1de

Browse files
Copilotjeswr
authored andcommitted
Add free text input for custom IDP URLs and remove Local CSS option
Co-authored-by: jeswr <63333554+jeswr@users.noreply.github.com>
1 parent 58d1c2a commit 4fab1de

1 file changed

Lines changed: 147 additions & 27 deletions

File tree

app/components/LoginPage.tsx

Lines changed: 147 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import { useState, useRef, useEffect } from "react";
44
import { useSolidAuth } from "@ldo/solid-react";
55
import { useRouter } from "next/navigation";
66
import Image from "next/image";
77
import Button from "./shared/Button";
8+
import { ChevronDownIcon } from "@heroicons/react/24/outline";
89

9-
const OIDC_ISSUERS = [
10+
const PRESET_ISSUERS = [
1011
{ label: "Solid Community", value: "https://solidcommunity.net/" },
1112
{ label: "Inrupt", value: "https://login.inrupt.com" },
12-
{ label: "Local CSS (ACP)", value: "http://localhost:3000/" },
1313
] as const;
1414

1515
export default function LoginPage() {
1616
const { session, login } = useSolidAuth();
1717
const router = useRouter();
18-
const [selectedIssuer, setSelectedIssuer] = useState<string>(
19-
process.env.NEXT_PUBLIC_OIDC_ISSUER || OIDC_ISSUERS[0].value
18+
const [issuerInput, setIssuerInput] = useState<string>(
19+
process.env.NEXT_PUBLIC_OIDC_ISSUER || ""
2020
);
2121
const [isLoading, setIsLoading] = useState(false);
22+
const [showDropdown, setShowDropdown] = useState(false);
23+
const [error, setError] = useState<string | null>(null);
24+
const inputRef = useRef<HTMLInputElement>(null);
25+
const dropdownRef = useRef<HTMLDivElement>(null);
2226

2327
// Redirect to home if already authenticated
2428
useEffect(() => {
@@ -32,16 +36,85 @@ export default function LoginPage() {
3236
return null;
3337
}
3438

39+
// Close dropdown when clicking outside
40+
useEffect(() => {
41+
const handleClickOutside = (event: MouseEvent) => {
42+
if (
43+
dropdownRef.current &&
44+
!dropdownRef.current.contains(event.target as Node) &&
45+
inputRef.current &&
46+
!inputRef.current.contains(event.target as Node)
47+
) {
48+
setShowDropdown(false);
49+
}
50+
};
51+
52+
if (showDropdown) {
53+
document.addEventListener("mousedown", handleClickOutside);
54+
return () => document.removeEventListener("mousedown", handleClickOutside);
55+
}
56+
}, [showDropdown]);
57+
58+
const validateIssuerUrl = (url: string): boolean => {
59+
if (!url.trim()) {
60+
setError("Please enter a Solid Identity Provider URL");
61+
return false;
62+
}
63+
64+
try {
65+
const parsedUrl = new URL(url);
66+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
67+
setError("URL must start with http:// or https://");
68+
return false;
69+
}
70+
} catch {
71+
setError("Please enter a valid URL");
72+
return false;
73+
}
74+
75+
setError(null);
76+
return true;
77+
};
78+
3579
const handleLogin = async () => {
80+
const trimmedIssuer = issuerInput.trim();
81+
if (!validateIssuerUrl(trimmedIssuer)) {
82+
return;
83+
}
84+
3685
setIsLoading(true);
3786
try {
38-
await login(selectedIssuer);
87+
await login(trimmedIssuer);
3988
} catch (error) {
4089
console.error("Login failed:", error);
4190
setIsLoading(false);
4291
}
4392
};
4493

94+
const handleIssuerSelect = (value: string) => {
95+
setIssuerInput(value);
96+
setShowDropdown(false);
97+
setError(null);
98+
inputRef.current?.focus();
99+
};
100+
101+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
102+
setIssuerInput(e.target.value);
103+
if (error) {
104+
setError(null);
105+
}
106+
};
107+
108+
// Filter preset issuers based on input
109+
const filteredIssuers = PRESET_ISSUERS.filter((issuer) => {
110+
if (!issuerInput.trim()) return true;
111+
const query = issuerInput.toLowerCase();
112+
return (
113+
issuer.label.toLowerCase().includes(query) ||
114+
issuer.value.toLowerCase().includes(query)
115+
);
116+
});
117+
45118
return (
46119
<main className="flex min-h-screen bg-white" role="main" aria-label="Sign in page">
47120
{/* Left side - Logo and branding */}
@@ -102,34 +175,82 @@ export default function LoginPage() {
102175
aria-label="Sign in form"
103176
noValidate
104177
>
105-
{/* Identity Provider Selection */}
178+
{/* Identity Provider Input */}
106179
<div>
107180
<label
108181
htmlFor="oidc-issuer"
109182
className="mb-2 block text-sm font-medium text-black"
110183
>
111184
Solid Identity Provider
112185
</label>
113-
<select
114-
id="oidc-issuer"
115-
name="oidc-issuer"
116-
value={selectedIssuer}
117-
onChange={(e) => setSelectedIssuer(e.target.value)}
118-
className="h-12 w-full cursor-pointer rounded-md border border-gray-300 bg-white px-4 text-black focus:border-[#7B42F6] focus:outline-none focus:ring-1 focus:ring-[#7B42F6] disabled:cursor-not-allowed disabled:opacity-50"
119-
disabled={isLoading}
120-
required
121-
aria-required="true"
122-
aria-label="Select Solid Identity Provider"
123-
aria-describedby="oidc-issuer-description"
124-
>
125-
{OIDC_ISSUERS.map((issuer) => (
126-
<option key={issuer.value} value={issuer.value}>
127-
{issuer.label}
128-
</option>
129-
))}
130-
</select>
186+
<div className="relative">
187+
<input
188+
ref={inputRef}
189+
id="oidc-issuer"
190+
name="oidc-issuer"
191+
type="text"
192+
value={issuerInput}
193+
onChange={handleInputChange}
194+
onFocus={() => setShowDropdown(true)}
195+
placeholder="Enter your provider URL or select from the list"
196+
className={`h-12 w-full rounded-md border bg-white px-4 pr-10 text-black placeholder:text-gray-500 focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 ${
197+
error
198+
? "border-red-300 focus:border-red-500 focus:ring-red-500"
199+
: "border-gray-300 focus:border-[#7B42F6] focus:ring-[#7B42F6]"
200+
}`}
201+
disabled={isLoading}
202+
required
203+
aria-required="true"
204+
aria-label="Enter or select Solid Identity Provider"
205+
aria-describedby={error ? "oidc-issuer-error" : "oidc-issuer-description"}
206+
aria-invalid={!!error}
207+
autoComplete="url"
208+
/>
209+
<button
210+
type="button"
211+
onClick={() => setShowDropdown(!showDropdown)}
212+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
213+
aria-label="Show provider options"
214+
tabIndex={-1}
215+
>
216+
<ChevronDownIcon className={`h-5 w-5 transition-transform ${showDropdown ? "rotate-180" : ""}`} />
217+
</button>
218+
219+
{/* Dropdown with preset options */}
220+
{showDropdown && filteredIssuers.length > 0 && (
221+
<div
222+
ref={dropdownRef}
223+
className="absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-white shadow-lg max-h-60 overflow-auto"
224+
role="listbox"
225+
aria-label="Preset identity providers"
226+
>
227+
{filteredIssuers.map((issuer) => (
228+
<button
229+
key={issuer.value}
230+
type="button"
231+
onClick={() => handleIssuerSelect(issuer.value)}
232+
className="w-full px-4 py-3 text-left hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
233+
role="option"
234+
aria-selected={issuerInput === issuer.value}
235+
>
236+
<div className="text-sm font-medium text-gray-900">
237+
{issuer.label}
238+
</div>
239+
<div className="text-xs text-gray-500">
240+
{issuer.value}
241+
</div>
242+
</button>
243+
))}
244+
</div>
245+
)}
246+
</div>
247+
{error && (
248+
<p id="oidc-issuer-error" className="mt-1 text-xs text-red-600" role="alert">
249+
{error}
250+
</p>
251+
)}
131252
<p id="oidc-issuer-description" className="sr-only">
132-
Choose your Solid Identity Provider to sign in
253+
Enter your Solid Identity Provider URL or select from the preset options
133254
</p>
134255
</div>
135256

@@ -151,4 +272,3 @@ export default function LoginPage() {
151272
</main>
152273
);
153274
}
154-

0 commit comments

Comments
 (0)