|
1 | | -import { use, Suspense } from "react"; |
2 | | -import { |
3 | | - Router, |
4 | | - Outlet, |
5 | | - useLocation, |
6 | | - useSearchParams, |
7 | | - useNavigate, |
8 | | - route, |
9 | | -} from "@funstack/router"; |
| 1 | +import { Router, route } from "@funstack/router"; |
| 2 | +import { Layout } from "./shared"; |
| 3 | +import { homeRoute } from "./features/home"; |
| 4 | +import { aboutRoute } from "./features/about"; |
| 5 | +import { usersRoutes } from "./features/users"; |
| 6 | +import { searchRoute } from "./features/search"; |
10 | 7 |
|
11 | | -// Types |
12 | | -type User = { id: string; name: string; role: string }; |
13 | | - |
14 | | -// Sample user data (simulating a database) |
15 | | -const users: User[] = [ |
16 | | - { id: "1", name: "Alice", role: "Admin" }, |
17 | | - { id: "2", name: "Bob", role: "User" }, |
18 | | - { id: "3", name: "Charlie", role: "User" }, |
19 | | -]; |
20 | | - |
21 | | -// Simulated async data fetching (would be API calls in real app) |
22 | | -async function fetchUsers(): Promise<User[]> { |
23 | | - // Simulate network delay |
24 | | - await new Promise((resolve) => setTimeout(resolve, 500)); |
25 | | - return users; |
26 | | -} |
27 | | - |
28 | | -async function fetchUser(id: string): Promise<User | null> { |
29 | | - // Simulate network delay |
30 | | - await new Promise((resolve) => setTimeout(resolve, 300)); |
31 | | - return users.find((u) => u.id === id) || null; |
32 | | -} |
33 | | - |
34 | | -// Layout component with navigation |
35 | | -function Layout() { |
36 | | - const location = useLocation(); |
37 | | - |
38 | | - return ( |
39 | | - <div> |
40 | | - <nav> |
41 | | - {/* Native <a> tags work for basic navigation thanks to Navigation API */} |
42 | | - <a href="/">Home</a> |
43 | | - <a href="/about">About</a> |
44 | | - <a href="/users">Users</a> |
45 | | - <a href="/search?q=react">Search</a> |
46 | | - </nav> |
47 | | - <main> |
48 | | - <p style={{ color: "#666", fontSize: "0.9rem" }}> |
49 | | - Current path: <code>{location.pathname}</code> |
50 | | - {location.search && ( |
51 | | - <> |
52 | | - {" "} |
53 | | - | Search: <code>{location.search}</code> |
54 | | - </> |
55 | | - )} |
56 | | - </p> |
57 | | - <Outlet /> |
58 | | - </main> |
59 | | - </div> |
60 | | - ); |
61 | | -} |
62 | | - |
63 | | -// Home page |
64 | | -function Home() { |
65 | | - return ( |
66 | | - <div> |
67 | | - <h1>Welcome to FUNSTACK Router</h1> |
68 | | - <p>This is a demo of the router based on the Navigation API.</p> |
69 | | - <h2>Features demonstrated:</h2> |
70 | | - <ul> |
71 | | - <li> |
72 | | - <a href="/about">Basic navigation</a> (native <a> tag) |
73 | | - </li> |
74 | | - <li> |
75 | | - <a href="/users">Data loaders with Suspense</a> - async data fetching |
76 | | - </li> |
77 | | - <li> |
78 | | - <a href="/users/1">Route parameters with loaders</a> |
79 | | - </li> |
80 | | - <li> |
81 | | - <a href="/search?q=hello&page=1">Search parameters</a> |
82 | | - </li> |
83 | | - <li> |
84 | | - See About page for <code>useNavigate()</code> hook demo |
85 | | - </li> |
86 | | - </ul> |
87 | | - <h2>Data Loader Features:</h2> |
88 | | - <ul> |
89 | | - <li> |
90 | | - Loaders execute before component renders (parallel with navigation) |
91 | | - </li> |
92 | | - <li>Results are cached - instant back/forward navigation</li> |
93 | | - <li>Components receive loader result as a prop</li> |
94 | | - <li> |
95 | | - For async loaders, use React's <code>use()</code> hook with Suspense |
96 | | - </li> |
97 | | - </ul> |
98 | | - </div> |
99 | | - ); |
100 | | -} |
101 | | - |
102 | | -// About page |
103 | | -function About() { |
104 | | - const navigate = useNavigate(); |
105 | | - |
106 | | - return ( |
107 | | - <div> |
108 | | - <h1>About</h1> |
109 | | - <p> |
110 | | - FUNSTACK Router is a modern React router built on the Navigation API. |
111 | | - </p> |
112 | | - <button onClick={() => navigate("/")}>Go Home (programmatic)</button> |
113 | | - </div> |
114 | | - ); |
115 | | -} |
116 | | - |
117 | | -// Users list page - receives Promise from loader, uses React's use() to suspend |
118 | | -function Users({ data }: { data: Promise<User[]> }) { |
119 | | - const userList = use(data); |
120 | | - |
121 | | - return ( |
122 | | - <div> |
123 | | - <h1>Users</h1> |
124 | | - <p style={{ color: "#666", fontSize: "0.9rem" }}> |
125 | | - (Data loaded via async loader with Suspense) |
126 | | - </p> |
127 | | - <div> |
128 | | - {userList.map((user) => ( |
129 | | - <div key={user.id} className="user-card"> |
130 | | - <strong>{user.name}</strong> - {user.role} |
131 | | - <br /> |
132 | | - <a href={`/users/${user.id}`}>View Profile</a> |
133 | | - </div> |
134 | | - ))} |
135 | | - </div> |
136 | | - </div> |
137 | | - ); |
138 | | -} |
139 | | - |
140 | | -// User detail page - receives Promise from loader |
141 | | -function UserDetail({ data }: { data: Promise<User | null> }) { |
142 | | - const user = use(data); |
143 | | - const navigate = useNavigate(); |
144 | | - |
145 | | - if (!user) { |
146 | | - return ( |
147 | | - <div> |
148 | | - <h1>User Not Found</h1> |
149 | | - <p>The requested user does not exist.</p> |
150 | | - <button onClick={() => navigate("/users")}>Back to Users</button> |
151 | | - </div> |
152 | | - ); |
153 | | - } |
154 | | - |
155 | | - return ( |
156 | | - <div> |
157 | | - <h1>{user.name}</h1> |
158 | | - <p style={{ color: "#666", fontSize: "0.9rem" }}> |
159 | | - (Data loaded via async loader with Suspense) |
160 | | - </p> |
161 | | - <p> |
162 | | - <strong>ID:</strong> {user.id} |
163 | | - </p> |
164 | | - <p> |
165 | | - <strong>Role:</strong> {user.role} |
166 | | - </p> |
167 | | - <button onClick={() => navigate("/users")}>Back to Users</button> |
168 | | - </div> |
169 | | - ); |
170 | | -} |
171 | | - |
172 | | -// Search page demonstrating useSearchParams |
173 | | -function Search() { |
174 | | - const [searchParams, setSearchParams] = useSearchParams(); |
175 | | - const query = searchParams.get("q") || ""; |
176 | | - const page = searchParams.get("page") || "1"; |
177 | | - |
178 | | - return ( |
179 | | - <div> |
180 | | - <h1>Search</h1> |
181 | | - <p> |
182 | | - <strong>Query:</strong> {query || "(none)"} |
183 | | - </p> |
184 | | - <p> |
185 | | - <strong>Page:</strong> {page} |
186 | | - </p> |
187 | | - |
188 | | - <div style={{ marginTop: "1rem" }}> |
189 | | - <input |
190 | | - type="text" |
191 | | - value={query} |
192 | | - onChange={(e) => |
193 | | - setSearchParams((prev) => { |
194 | | - prev.set("q", e.target.value); |
195 | | - return prev; |
196 | | - }) |
197 | | - } |
198 | | - placeholder="Search..." |
199 | | - style={{ padding: "0.5rem", marginRight: "0.5rem" }} |
200 | | - /> |
201 | | - <button |
202 | | - onClick={() => |
203 | | - setSearchParams((prev) => { |
204 | | - prev.set("page", String(Number(page) + 1)); |
205 | | - return prev; |
206 | | - }) |
207 | | - } |
208 | | - > |
209 | | - Next Page |
210 | | - </button> |
211 | | - </div> |
212 | | - </div> |
213 | | - ); |
214 | | -} |
215 | | - |
216 | | -// 404 page |
217 | | -function NotFound() { |
218 | | - const location = useLocation(); |
219 | | - |
220 | | - return ( |
221 | | - <div> |
222 | | - <h1>404 - Not Found</h1> |
223 | | - <p> |
224 | | - The page <code>{location.pathname}</code> does not exist. |
225 | | - </p> |
226 | | - <a href="/">Go Home</a> |
227 | | - </div> |
228 | | - ); |
229 | | -} |
230 | | - |
231 | | -// Route configuration using route() helper for type safety |
232 | 8 | const routes = [ |
233 | 9 | route({ |
234 | 10 | path: "/", |
235 | 11 | component: Layout, |
236 | | - children: [ |
237 | | - route({ path: "", component: Home }), |
238 | | - route({ path: "about", component: About }), |
239 | | - // Async loader - component receives Promise and uses use() to suspend |
240 | | - route({ |
241 | | - path: "users", |
242 | | - component: Users, |
243 | | - loader: () => fetchUsers(), |
244 | | - }), |
245 | | - // Async loader with params |
246 | | - route({ |
247 | | - path: "users/:id", |
248 | | - component: UserDetail, |
249 | | - loader: ({ params }) => fetchUser(params.id), |
250 | | - }), |
251 | | - route({ path: "search", component: Search }), |
252 | | - ], |
| 12 | + children: [homeRoute, aboutRoute, ...usersRoutes, searchRoute], |
253 | 13 | }), |
254 | 14 | ]; |
255 | 15 |
|
256 | | -// Loading fallback for Suspense |
257 | | -function LoadingSpinner() { |
258 | | - return ( |
259 | | - <div style={{ padding: "2rem", textAlign: "center" }}> |
260 | | - <p>Loading...</p> |
261 | | - </div> |
262 | | - ); |
263 | | -} |
264 | | - |
265 | 16 | export function App() { |
266 | | - return ( |
267 | | - <Suspense fallback={<LoadingSpinner />}> |
268 | | - <Router routes={routes} /> |
269 | | - </Suspense> |
270 | | - ); |
| 17 | + return <Router routes={routes} />; |
271 | 18 | } |
0 commit comments