Skip to content

Commit 46d9bba

Browse files
Copilotjeswr
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 ea9dab6 commit 46d9bba

1 file changed

Lines changed: 147 additions & 26 deletions

File tree

app/components/LoginPage.tsx

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

3-
import { useState } from "react";
3+
import { useState, useRef, useEffect } from "react";
44
import { login } from "@inrupt/solid-client-authn-browser";
55
import Image from "next/image";
66
import Button from "./shared/Button";
7+
import { ChevronDownIcon } from "@heroicons/react/24/outline";
78

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

1414
export default function LoginPage() {
15-
const [selectedIssuer, setSelectedIssuer] = useState<string>(
16-
process.env.NEXT_PUBLIC_OIDC_ISSUER || OIDC_ISSUERS[0].value
15+
const [issuerInput, setIssuerInput] = useState<string>(
16+
process.env.NEXT_PUBLIC_OIDC_ISSUER || ""
1717
);
1818
const [isLoading, setIsLoading] = useState(false);
19+
const [showDropdown, setShowDropdown] = useState(false);
20+
const [error, setError] = useState<string | null>(null);
21+
const inputRef = useRef<HTMLInputElement>(null);
22+
const dropdownRef = useRef<HTMLDivElement>(null);
23+
24+
// Close dropdown when clicking outside
25+
useEffect(() => {
26+
const handleClickOutside = (event: MouseEvent) => {
27+
if (
28+
dropdownRef.current &&
29+
!dropdownRef.current.contains(event.target as Node) &&
30+
inputRef.current &&
31+
!inputRef.current.contains(event.target as Node)
32+
) {
33+
setShowDropdown(false);
34+
}
35+
};
36+
37+
if (showDropdown) {
38+
document.addEventListener("mousedown", handleClickOutside);
39+
return () => document.removeEventListener("mousedown", handleClickOutside);
40+
}
41+
}, [showDropdown]);
42+
43+
const validateIssuerUrl = (url: string): boolean => {
44+
if (!url.trim()) {
45+
setError("Please enter a Solid Identity Provider URL");
46+
return false;
47+
}
48+
49+
try {
50+
const parsedUrl = new URL(url);
51+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
52+
setError("URL must start with http:// or https://");
53+
return false;
54+
}
55+
} catch {
56+
setError("Please enter a valid URL");
57+
return false;
58+
}
59+
60+
setError(null);
61+
return true;
62+
};
1963

2064
const handleLogin = async () => {
65+
const trimmedIssuer = issuerInput.trim();
66+
if (!validateIssuerUrl(trimmedIssuer)) {
67+
return;
68+
}
69+
2170
setIsLoading(true);
2271
try {
2372
const baseUrl = window.location.origin + window.location.pathname;
2473
await login({
25-
oidcIssuer: selectedIssuer,
74+
oidcIssuer: trimmedIssuer,
2675
clientName: "Solid File Manager",
2776
redirectUrl: baseUrl,
2877
});
@@ -32,6 +81,30 @@ export default function LoginPage() {
3281
}
3382
};
3483

84+
const handleIssuerSelect = (value: string) => {
85+
setIssuerInput(value);
86+
setShowDropdown(false);
87+
setError(null);
88+
inputRef.current?.focus();
89+
};
90+
91+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
92+
setIssuerInput(e.target.value);
93+
if (error) {
94+
setError(null);
95+
}
96+
};
97+
98+
// Filter preset issuers based on input
99+
const filteredIssuers = PRESET_ISSUERS.filter((issuer) => {
100+
if (!issuerInput.trim()) return true;
101+
const query = issuerInput.toLowerCase();
102+
return (
103+
issuer.label.toLowerCase().includes(query) ||
104+
issuer.value.toLowerCase().includes(query)
105+
);
106+
});
107+
35108
return (
36109
<main className="flex min-h-screen bg-white" role="main" aria-label="Sign in page">
37110
{/* Left side - Logo and branding */}
@@ -92,34 +165,82 @@ export default function LoginPage() {
92165
aria-label="Sign in form"
93166
noValidate
94167
>
95-
{/* Identity Provider Selection */}
168+
{/* Identity Provider Input */}
96169
<div>
97170
<label
98171
htmlFor="oidc-issuer"
99172
className="mb-2 block text-sm font-medium text-black"
100173
>
101174
Solid Identity Provider
102175
</label>
103-
<select
104-
id="oidc-issuer"
105-
name="oidc-issuer"
106-
value={selectedIssuer}
107-
onChange={(e) => setSelectedIssuer(e.target.value)}
108-
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"
109-
disabled={isLoading}
110-
required
111-
aria-required="true"
112-
aria-label="Select Solid Identity Provider"
113-
aria-describedby="oidc-issuer-description"
114-
>
115-
{OIDC_ISSUERS.map((issuer) => (
116-
<option key={issuer.value} value={issuer.value}>
117-
{issuer.label}
118-
</option>
119-
))}
120-
</select>
176+
<div className="relative">
177+
<input
178+
ref={inputRef}
179+
id="oidc-issuer"
180+
name="oidc-issuer"
181+
type="text"
182+
value={issuerInput}
183+
onChange={handleInputChange}
184+
onFocus={() => setShowDropdown(true)}
185+
placeholder="Enter your provider URL or select from the list"
186+
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 ${
187+
error
188+
? "border-red-300 focus:border-red-500 focus:ring-red-500"
189+
: "border-gray-300 focus:border-[#7B42F6] focus:ring-[#7B42F6]"
190+
}`}
191+
disabled={isLoading}
192+
required
193+
aria-required="true"
194+
aria-label="Enter or select Solid Identity Provider"
195+
aria-describedby={error ? "oidc-issuer-error" : "oidc-issuer-description"}
196+
aria-invalid={!!error}
197+
autoComplete="url"
198+
/>
199+
<button
200+
type="button"
201+
onClick={() => setShowDropdown(!showDropdown)}
202+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
203+
aria-label="Show provider options"
204+
tabIndex={-1}
205+
>
206+
<ChevronDownIcon className={`h-5 w-5 transition-transform ${showDropdown ? "rotate-180" : ""}`} />
207+
</button>
208+
209+
{/* Dropdown with preset options */}
210+
{showDropdown && filteredIssuers.length > 0 && (
211+
<div
212+
ref={dropdownRef}
213+
className="absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-white shadow-lg max-h-60 overflow-auto"
214+
role="listbox"
215+
aria-label="Preset identity providers"
216+
>
217+
{filteredIssuers.map((issuer) => (
218+
<button
219+
key={issuer.value}
220+
type="button"
221+
onClick={() => handleIssuerSelect(issuer.value)}
222+
className="w-full px-4 py-3 text-left hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
223+
role="option"
224+
aria-selected={issuerInput === issuer.value}
225+
>
226+
<div className="text-sm font-medium text-gray-900">
227+
{issuer.label}
228+
</div>
229+
<div className="text-xs text-gray-500">
230+
{issuer.value}
231+
</div>
232+
</button>
233+
))}
234+
</div>
235+
)}
236+
</div>
237+
{error && (
238+
<p id="oidc-issuer-error" className="mt-1 text-xs text-red-600" role="alert">
239+
{error}
240+
</p>
241+
)}
121242
<p id="oidc-issuer-description" className="sr-only">
122-
Choose your Solid Identity Provider to sign in
243+
Enter your Solid Identity Provider URL or select from the preset options
123244
</p>
124245
</div>
125246

0 commit comments

Comments
 (0)