Skip to content

Commit cb37af3

Browse files
authored
feat: ui (#28)
* fix: fonts * feat: vibed sparkle effect * fix: spacing * fix: copy * fix: security page * fix: security page * fix: header * fix: checkpoint * fix: authcontext * fix: align * fix: copy to clipboard * fix: header link
1 parent 5c1d688 commit cb37af3

20 files changed

+860
-419
lines changed
239 KB
Binary file not shown.
222 KB
Binary file not shown.

frontend/src/App.tsx

Lines changed: 64 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
import React, { useState } from "react";
2-
import { Routes, Route, Navigate, Outlet } from "react-router-dom"; // Import routing components
3-
import AppsManager from "./components/AppsManager";
4-
import LoginPage from "./pages/Login"; // Import LoginPage
5-
import SignupPage from "./pages/Signup"; // Import SignupPage
6-
import Logo from "./assets/logo.svg";
1+
import AppsManager from "@/components/AppsManager";
2+
import { ShieldIcon } from "lucide-react";
3+
import React from "react"; // Removed useState
4+
import { Link, Navigate, Outlet, Route, Routes } from "react-router-dom"; // Import routing components
75
import { toast } from "sonner";
6+
import Logo from "./assets/logo.svg";
87
import { MnemonicManager } from "./components/MnemonicManager";
9-
import { ShieldIcon } from "lucide-react";
8+
import SparkleEffect from "./components/SparkleEffect";
109
import { Button } from "./components/ui/button";
10+
import { useAuth } from "./context/AuthContext"; // Import useAuth
11+
import LoginPage from "./pages/Login"; // Import LoginPage
12+
import SignupPage from "./pages/Signup"; // Import SignupPage
1113

1214
// A component to protect routes that require authentication
13-
const ProtectedRoute = ({
14-
token,
15-
children,
16-
}: {
17-
token: string | null;
18-
children: React.ReactNode;
19-
}) => {
15+
// Refactored ProtectedRoute to use AuthContext
16+
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
17+
const { token, isLoading } = useAuth(); // Use context
18+
19+
// Show loading indicator while checking auth status
20+
if (isLoading) {
21+
return <div>Loading authentication...</div>; // Or a proper spinner/skeleton
22+
}
23+
24+
// If not loading and no token, redirect to login
2025
if (!token) {
2126
// Redirect them to the /login page, but save the current location they were
2227
// trying to go to when they were redirected. This allows us to send them
@@ -25,36 +30,38 @@ const ProtectedRoute = ({
2530
return <Navigate to="/login" replace />;
2631
}
2732

28-
return <>{children}</>; // Render children if token exists
33+
// If loading is finished and token exists, render the children
34+
return <>{children}</>;
2935
};
3036

3137
function App() {
32-
// Keep token state here to manage authentication globally
33-
const [token, setToken] = useState<string | null>(
34-
localStorage.getItem("authToken")
35-
); // Initialize from localStorage
36-
37-
const handleSetToken = (newToken: string | null) => {
38-
setToken(newToken);
39-
if (newToken) {
40-
localStorage.setItem("authToken", newToken); // Store token in localStorage
41-
} else {
42-
localStorage.removeItem("authToken"); // Remove token from localStorage on logout
43-
}
44-
};
45-
46-
const [showSecurity, setShowSecurity] = React.useState(false);
47-
const toggleSecurity = () => setShowSecurity((current) => !current);
38+
// Use the Auth context - removed isLoading as it's handled in ProtectedRoute now
39+
const { token, logout } = useAuth();
4840

41+
// Handle logout using context
4942
const handleLogout = () => {
50-
handleSetToken(null);
43+
logout();
5144
toast("Logged out.");
52-
// No need to clear username/password state as it's managed in pages now
5345
};
5446

47+
// Optional: Show a loading state while the token is being loaded from localStorage
48+
// This prevents flashing the login page briefly for authenticated users.
49+
// However, the ProtectedRoute will also handle loading state.
50+
// if (isLoading) {
51+
// return <div>Loading...</div>; // Or a spinner component
52+
// }
53+
5554
return (
56-
<div className="flex flex-col items-center justify-center min-h-screen py-8">
57-
<img src={Logo} className="mb-5" alt="Logo" /> {/* Added alt text */}
55+
<div className="font-sans flex flex-col items-center justify-center min-h-screen py-8">
56+
<SparkleEffect count={70} />
57+
<header className="flex flex-col gap-3 items-center justify-center mb-10">
58+
<Link to="/" className="z-10">
59+
<img src={Logo} alt="Logo" />
60+
</Link>
61+
<p className="text-muted-foreground">
62+
Simple web bitcoin wallet that connects to apps
63+
</p>
64+
</header>
5865
<Routes>
5966
{/* Public routes */}
6067
<Route
@@ -63,7 +70,7 @@ function App() {
6370
token ? (
6471
<Navigate to="/" />
6572
) : (
66-
<LoginPage setToken={handleSetToken} />
73+
<LoginPage /> // Remove setToken prop
6774
)
6875
}
6976
/>
@@ -73,7 +80,7 @@ function App() {
7380
token ? (
7481
<Navigate to="/" />
7582
) : (
76-
<SignupPage setToken={handleSetToken} />
83+
<SignupPage /> // Remove setToken prop
7784
)
7885
}
7986
/>
@@ -82,46 +89,32 @@ function App() {
8289
<Route
8390
path="/"
8491
element={
85-
<ProtectedRoute token={token}>
86-
{/* Outlet renders nested routes or the main content */}
87-
<Outlet />
88-
</ProtectedRoute>
89-
}
90-
>
91-
{/* Default protected route content (e.g., dashboard) */}
92-
<Route
93-
index
94-
element={
95-
<div className="w-full max-w-4xl px-4">
96-
{" "}
97-
{/* Added container for content */}
98-
{/* Removed welcome message, AppsManager is the main content now */}
99-
{/* <p>
100-
Token: <code>{token}</code>
101-
</p> */}
102-
<div className="flex justify-end mb-8 gap-4 -mt-14">
103-
<Button
104-
variant="outline"
105-
onClick={toggleSecurity} size="icon">
106-
<ShieldIcon />
107-
</Button>
108-
<Button
109-
className="backdrop-blur-xs"
110-
variant="outline"
111-
onClick={handleLogout}>
92+
<>
93+
<div className="w-full max-w-screen-md">
94+
<div className="flex justify-end gap-4 -mt-29">
95+
<Link to="/security">
96+
<Button variant="outline" size="icon">
97+
<ShieldIcon />
98+
</Button>
99+
</Link>
100+
<Button variant="outline" onClick={handleLogout}>
112101
Logout
113102
</Button>
114103
</div>
115-
{token && <AppsManager token={token} />}
116-
{/* Render AppsManager only if token exists */}
117-
{token && showSecurity && <MnemonicManager token={token} />}
118104
</div>
119-
}
120-
/>
121-
{/* Add other protected routes here if needed */}
105+
<div className="flex-1 w-full max-w-screen-md">
106+
<ProtectedRoute>
107+
<Outlet />
108+
</ProtectedRoute>
109+
</div>
110+
</>
111+
}
112+
>
113+
<Route index element={<AppsManager />} />
114+
<Route path="/security" element={<MnemonicManager />} />
122115
</Route>
123116

124-
{/* Fallback for unknown routes */}
117+
{/* Fallback for unknown routes - use token from context */}
125118
<Route
126119
path="*"
127120
element={<Navigate to={token ? "/" : "/login"} replace />}

frontend/src/components/AppsManager.tsx

Lines changed: 39 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
1-
import React, { useState, useEffect } from "react";
1+
import { useAuth } from "@/context/AuthContext";
2+
import { copyToClipboard } from "@/lib/clipboard";
3+
import { CirclePlus, Copy } from "lucide-react";
24
import { QRCodeCanvas } from "qrcode.react"; // Import QR Code component
5+
import React, { useEffect, useState } from "react";
36
import { Button } from "./ui/button";
4-
import { CirclePlus } from "lucide-react";
5-
import { Input } from "./ui/input";
67
import {
78
Card,
89
CardContent,
10+
CardDescription,
911
CardFooter,
1012
CardHeader,
1113
CardTitle,
1214
} from "./ui/card";
15+
import { Input } from "./ui/input";
1316

1417
interface App {
1518
name: string;
1619
pubkey: string;
1720
// Add other relevant app properties if needed
1821
}
1922

20-
// Define props for the component, including the token
21-
interface AppsManagerProps {
22-
token: string | null; // Allow null initially or if logged out
23-
}
24-
25-
const AppsManager: React.FC<AppsManagerProps> = ({ token }) => {
23+
const AppsManager: React.FC = () => {
24+
const { token } = useAuth();
2625
const [apps, setApps] = useState<App[]>([]);
2726
const [newAppName, setNewAppName] = useState("");
2827
const [isLoading, setIsLoading] = useState(true);
@@ -118,24 +117,8 @@ const AppsManager: React.FC<AppsManagerProps> = ({ token }) => {
118117
}
119118
};
120119

121-
const copyToClipboard = () => {
122-
if (lastCreatedNwcUrl) {
123-
navigator.clipboard
124-
.writeText(lastCreatedNwcUrl)
125-
.then(() => {
126-
setCopySuccess("Copied!");
127-
setTimeout(() => setCopySuccess(""), 2000); // Clear message after 2s
128-
})
129-
.catch((err) => {
130-
console.error("Failed to copy text: ", err);
131-
setCopySuccess("Failed to copy");
132-
setTimeout(() => setCopySuccess(""), 2000);
133-
});
134-
}
135-
};
136-
137120
return (
138-
<div>
121+
<div className="w-full max-w-screen-md">
139122
{/* Form to create a new app */}
140123
<form onSubmit={handleCreateApp}>
141124
<Card>
@@ -147,7 +130,7 @@ const AppsManager: React.FC<AppsManagerProps> = ({ token }) => {
147130
type="text"
148131
value={newAppName}
149132
onChange={(e) => setNewAppName(e.target.value)}
150-
placeholder="New App Name"
133+
placeholder="Name"
151134
required
152135
/>
153136
</CardContent>
@@ -161,45 +144,45 @@ const AppsManager: React.FC<AppsManagerProps> = ({ token }) => {
161144
</form>
162145
{/* Display NWC URL and QR code if available */}
163146
{lastCreatedNwcUrl && (
164-
<div
165-
style={{
166-
marginTop: "20px",
167-
padding: "15px",
168-
border: "1px solid #ccc",
169-
borderRadius: "5px",
170-
}}
171-
>
172-
<h3>New App Connection String:</h3>
173-
<textarea
174-
value={lastCreatedNwcUrl}
175-
readOnly
176-
style={{
177-
width: "90%",
178-
minHeight: "60px",
179-
marginBottom: "10px",
180-
fontFamily: "monospace",
181-
resize: "none",
182-
}}
183-
/>
184-
<button onClick={copyToClipboard} style={{ marginRight: "10px" }}>
185-
{copySuccess || "Copy URL"}
186-
</button>
187-
<div style={{ marginTop: "15px" }}>
188-
<QRCodeCanvas value={lastCreatedNwcUrl} size={128} />
189-
</div>
190-
</div>
147+
<Card className="mt-5">
148+
<CardHeader>
149+
<CardTitle>Connect your App</CardTitle>
150+
<CardDescription>
151+
Scan the QR Code or copy & paste the connection secret
152+
</CardDescription>
153+
</CardHeader>
154+
<CardContent className="flex flex-col gap-4 justify-start items-start">
155+
<div className="bg-white p-2 rounded-lg">
156+
<QRCodeCanvas value={lastCreatedNwcUrl} size={150} />
157+
</div>
158+
<div className="flex flex-row items-center gap-2 flex-1 w-full">
159+
<Input
160+
value={lastCreatedNwcUrl}
161+
readOnly
162+
className="font-mono w-full"
163+
/>
164+
<Button
165+
onClick={() => copyToClipboard(lastCreatedNwcUrl)}
166+
variant="outline"
167+
>
168+
<Copy className="w-4 h-4 mr-2" />
169+
Copy
170+
</Button>
171+
</div>
172+
</CardContent>
173+
</Card>
191174
)}
192175
{/* Display existing apps */}
193176
<h3 className="text-2xl font-semibold my-5">Connected Apps</h3>
194177
{isLoading && <p>Loading apps...</p>}
195178
{error && !lastCreatedNwcUrl && (
196-
<p style={{ color: "red" }}>Error: {error}</p>
179+
<p className="text-red-500">Error: {error}</p>
197180
)}{" "}
198181
{/* Show fetch error only if not showing NWC URL */}
199182
{!isLoading && !error && (
200183
<>
201184
{apps.length === 0 ? (
202-
<p>No apps connected yet.</p>
185+
<p className="text-muted-foreground">No apps connected yet.</p>
203186
) : (
204187
<div className="grid grid-cols-1 gap-3">
205188
{apps.map((app, index) => (

0 commit comments

Comments
 (0)