Skip to content

Commit 27ece54

Browse files
committed
wip - apps/web
1 parent acc9448 commit 27ece54

17 files changed

+1483
-95
lines changed

apps/api/tsconfig.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"compilerOptions": {
44
"types": ["node"]
55
},
6-
"references": [],
6+
"references": [
7+
{ "path": "../../packages/auth-service" },
8+
{ "path": "../../packages/planet-service" },
9+
{ "path": "../../packages/chat-service" }
10+
],
711
"include": ["src"],
812
"exclude": [
913
"**/*.test.*",

apps/web/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>oRPC multiservice monorepo playground</title>
7+
<title>oRPC Multiservice Monorepo | Playground</title>
8+
<meta name="description" content="oRPC multiservice monorepo playground with TanStack Query integration" />
89
</head>
910
<body>
1011
<div id="root"></div>

apps/web/src/App.tsx

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,94 @@
1-
import { RequireLogin } from './components/require-login'
1+
import { AuthGuard } from './components/AuthGuard'
2+
import { ChatRoom } from './components/ChatRoom'
3+
import { PlanetCreator } from './components/PlanetCreator'
4+
import { PlanetsList } from './components/PlanetsList'
25

36
function App() {
7+
const handleLogout = () => {
8+
localStorage.removeItem('authToken')
9+
window.location.reload()
10+
}
11+
412
return (
5-
<RequireLogin>
6-
<div>
7-
hello world
8-
</div>
9-
</RequireLogin>
13+
<AuthGuard>
14+
{/* Navigation Bar */}
15+
<nav>
16+
<div className="nav-logo">oRPC Playground</div>
17+
18+
<div className="nav-menu">
19+
<a href="#planets">Planets</a>
20+
<a href="#chat">Chat</a>
21+
</div>
22+
23+
<div className="nav-actions">
24+
<button className="btn-link" onClick={handleLogout}>
25+
Log Out
26+
</button>
27+
</div>
28+
</nav>
29+
30+
{/* Main Content */}
31+
<main className="container" style={{ paddingTop: '60px', paddingBottom: '60px' }}>
32+
{/* Hero Section */}
33+
<section style={{ marginBottom: '80px', textAlign: 'center' }}>
34+
<h1 style={{ marginBottom: '20px' }}>oRPC Multiservice Monorepo</h1>
35+
<p
36+
style={{
37+
fontSize: '16px',
38+
textTransform: 'uppercase',
39+
letterSpacing: '0.5px',
40+
color: '#666',
41+
fontWeight: 600,
42+
marginBottom: '40px',
43+
}}
44+
>
45+
TanStack Query Integration Playground
46+
</p>
47+
<div className="message message-success" style={{ maxWidth: '800px', margin: '0 auto' }}>
48+
✨ Explore oRPC with real-time chat, infinite queries, and form mutations
49+
</div>
50+
</section>
51+
52+
{/* Create Planet Section */}
53+
<section id="planets" style={{ marginBottom: '60px' }}>
54+
<PlanetCreator />
55+
</section>
56+
57+
{/* List Planets Section */}
58+
<section style={{ marginBottom: '60px' }}>
59+
<PlanetsList />
60+
</section>
61+
62+
{/* Chat Section */}
63+
<section id="chat">
64+
<ChatRoom />
65+
</section>
66+
</main>
67+
68+
{/* Footer */}
69+
<footer
70+
style={{
71+
marginTop: 'auto',
72+
padding: '40px',
73+
borderTop: '3px solid #1a1a1a',
74+
background: '#ffffff',
75+
textAlign: 'center',
76+
}}
77+
>
78+
<p
79+
style={{
80+
fontFamily: '\'Courier New\', Courier, monospace',
81+
fontSize: '14px',
82+
fontWeight: 600,
83+
textTransform: 'uppercase',
84+
letterSpacing: '0.5px',
85+
margin: 0,
86+
}}
87+
>
88+
Built with oRPC + TanStack Query
89+
</p>
90+
</footer>
91+
</AuthGuard>
1092
)
1193
}
1294

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import type { AuthServiceOutputs } from '../lib/service-auth'
2+
import { useEffect, useState } from 'react'
3+
import { authServiceClient } from '../lib/service-auth'
4+
import { InfoMessage } from './ui/InfoMessage'
5+
import { InterfaceWindow } from './ui/InterfaceWindow'
6+
import { LoadingSpinner } from './ui/LoadingSpinner'
7+
8+
interface AuthGuardProps {
9+
children: React.ReactNode
10+
}
11+
12+
export function AuthGuard({ children }: AuthGuardProps) {
13+
const [user, setUser] = useState<AuthServiceOutputs['auth']['me'] | null>(null)
14+
const [isLoading, setIsLoading] = useState(true)
15+
16+
useEffect(() => {
17+
const controller = new AbortController()
18+
19+
async function checkAuth() {
20+
try {
21+
const userData = await authServiceClient.auth.me(undefined, {
22+
signal: controller.signal,
23+
})
24+
setUser(userData)
25+
}
26+
catch (err) {
27+
if (!controller.signal.aborted) {
28+
console.error('Auth check failed:', err)
29+
// Don't show error, just show login
30+
}
31+
}
32+
finally {
33+
if (!controller.signal.aborted) {
34+
setIsLoading(false)
35+
}
36+
}
37+
}
38+
39+
void checkAuth()
40+
41+
return () => {
42+
controller.abort()
43+
}
44+
}, [])
45+
46+
if (isLoading) {
47+
return <LoadingSpinner text="Authenticating..." />
48+
}
49+
50+
if (!user) {
51+
return <LoginForm onLoginSuccess={setUser} />
52+
}
53+
54+
return <>{children}</>
55+
}
56+
57+
interface LoginFormProps {
58+
onLoginSuccess: (user: AuthServiceOutputs['auth']['me']) => void
59+
}
60+
61+
function LoginForm({ onLoginSuccess }: LoginFormProps) {
62+
const [isSubmitting, setIsSubmitting] = useState(false)
63+
const [error, setError] = useState<string | null>(null)
64+
65+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
66+
event.preventDefault()
67+
setIsSubmitting(true)
68+
setError(null)
69+
70+
const form = new FormData(event.currentTarget)
71+
const email = form.get('email') as string
72+
const password = form.get('password') as string
73+
74+
try {
75+
const { token } = await authServiceClient.auth.signin({
76+
email,
77+
password,
78+
})
79+
80+
localStorage.setItem('authToken', token)
81+
82+
const user = await authServiceClient.auth.me()
83+
onLoginSuccess(user)
84+
}
85+
catch (err) {
86+
const errorMessage = err instanceof Error ? err.message : 'Login failed'
87+
setError(errorMessage)
88+
console.error('Login error:', err)
89+
}
90+
finally {
91+
setIsSubmitting(false)
92+
}
93+
}
94+
95+
return (
96+
<div className="container" style={{ paddingTop: '80px', paddingBottom: '80px' }}>
97+
<InterfaceWindow
98+
tabs={[{ label: 'LOG IN', isActive: true }]}
99+
contentPadding={30}
100+
>
101+
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
102+
<InfoMessage className="mb-20">
103+
Please log in to continue.
104+
</InfoMessage>
105+
106+
{error && (
107+
<div className="message message-error mb-20">
108+
{error}
109+
</div>
110+
)}
111+
112+
<form onSubmit={handleSubmit}>
113+
<label>
114+
Email
115+
<input
116+
type="email"
117+
name="email"
118+
required
119+
placeholder="hi@gmail.com"
120+
disabled={isSubmitting}
121+
/>
122+
</label>
123+
124+
<label>
125+
Password
126+
<input
127+
type="password"
128+
name="password"
129+
required
130+
placeholder="Enter your password"
131+
disabled={isSubmitting}
132+
/>
133+
</label>
134+
135+
<button
136+
type="submit"
137+
className="btn-primary"
138+
style={{ width: '100%' }}
139+
disabled={isSubmitting}
140+
>
141+
{isSubmitting ? 'Logging in...' : 'Log In'}
142+
</button>
143+
</form>
144+
</div>
145+
</InterfaceWindow>
146+
</div>
147+
)
148+
}

0 commit comments

Comments
 (0)