Skip to content

Commit

Permalink
✨ feat: move from MPA to SPA using TanStack Router (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
vaayne committed Jan 2, 2025
1 parent ca1769e commit 2035326
Show file tree
Hide file tree
Showing 35 changed files with 637 additions and 208 deletions.
2 changes: 1 addition & 1 deletion internal/pkg/auth/oauth-provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (p *oAuthProvider) GetConfig() *oauth2.Config {
ClientID: p.cfg.Key,
ClientSecret: p.cfg.Secret,
Endpoint: p.endpoint,
RedirectURL: fmt.Sprintf("%s/auth.html", config.Settings.Service.Fqdn),
RedirectURL: fmt.Sprintf("%s/auth/oauth/%s/callback", config.Settings.Service.Fqdn, p.Name),
Scopes: append(p.defaultScopes, p.cfg.Scopes...),
}
}
Expand Down
39 changes: 35 additions & 4 deletions internal/port/httpserver/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package httpserver

import (
"net/http"
"recally/internal/pkg/logger"
"recally/web"

_ "recally/docs"
Expand Down Expand Up @@ -49,16 +48,48 @@ func (s *Service) registerRouters() {
// Swagger
e.GET("/swagger/*", echoSwagger.WrapHandler)

// web pages
logger.Default.Debug("Using static files as frontend")
// Web UI
s.registerWebUIRouters(e)
}

func (s *Service) registerWebUIRouters(e *echo.Echo) {
// manifest.webmanifest for PWA
e.GET("/manifest.webmanifest", func(c echo.Context) error {
file, err := web.StaticHttpFS.Open("manifest.webmanifest")
if err != nil {
return err
}
defer file.Close()
c.Response().Header().Set("Content-Type", "application/manifest+json")
c.Response().Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
return c.Stream(http.StatusOK, "application/manifest+json", file)
})
e.GET("/*", echo.WrapHandler(http.FileServer(web.StaticHttpFS)))

e.GET("/assets/*", func(c echo.Context) error {
c.Response().Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
return echo.WrapHandler(http.FileServer(web.StaticHttpFS))(c)
})

// Serve static files for SPA, if there is a static file, serve it, otherwise serve index.html
e.GET("/*", func(c echo.Context) error {
path := c.Param("*")
if path != "" {
// Try to open the requested file
if file, err := web.StaticHttpFS.Open(path); err == nil {
defer file.Close()
// Add cache control for static assets
return echo.WrapHandler(http.FileServer(web.StaticHttpFS))(c)
}
}

// If file not found, serve index.html
file, err := web.StaticHttpFS.Open("index.html")
if err != nil {
return err
}
defer file.Close()
// No cache for index.html to ensure latest version
c.Response().Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
return c.Stream(http.StatusOK, "text/html", file)
})
}
17 changes: 0 additions & 17 deletions web/auth.html

This file was deleted.

17 changes: 0 additions & 17 deletions web/bookmarks.html

This file was deleted.

Binary file modified web/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/pages/bookmarks.tsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.6",
"@tanstack/react-router": "^1.93.0",
"@types/js-cookie": "^3.0.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -53,6 +54,8 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/router-devtools": "^1.93.0",
"@tanstack/router-plugin": "^1.93.0",
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
Expand Down
Binary file added web/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/maskable-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 0 additions & 17 deletions web/settings.html

This file was deleted.

4 changes: 2 additions & 2 deletions web/src/pages/app-basic.tsx → web/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { type ReactNode, StrictMode } from "react";
import "../index.css";
import "./index.css";

import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster";
import { NuqsAdapter } from "nuqs/adapters/react";
import { SWRConfig } from "swr";
import fetcher from "../lib/apis/fetcher";
import fetcher from "./lib/apis/fetcher";

export default function App({ children }: { children: ReactNode }) {
return (
Expand Down
64 changes: 10 additions & 54 deletions web/src/pages/auth.tsx → web/src/components/auth/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useAuth, useUser } from "@/lib/apis/auth";
import { useAuth } from "@/lib/apis/auth";
import { ROUTES } from "@/lib/router";
import { SiGithub } from "@icons-pack/react-simple-icons";
import { Link, useRouter } from "@tanstack/react-router";
import { Mail } from "lucide-react";
import { parseAsString, useQueryState } from "nuqs";
import type React from "react";
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import App from "./app-basic";
import { useState } from "react";

interface AuthFormData {
email: string;
Expand Down Expand Up @@ -46,43 +44,18 @@ const OAuthProviders = [
// },
];

export default function AuthPage() {
// "login" or "register"
const [mode, _] = useQueryState(
"mode",
parseAsString.withDefault(AuthMode.Login),
);
export default function AuthComponent({ mode }: { mode: string }) {
const isLogin = mode === AuthMode.Login;

// oauth callback state and code
const [code] = useQueryState("code");
const [state] = useQueryState("state");

// email and password login form data
const [formData, setFormData] = useState<AuthFormData>({
email: "",
password: "",
...(isLogin ? {} : { confirmPassword: "", name: "" }),
});

const { login, register, oauthLogin, oauthCallback } = useAuth();

const { user } = useUser();

useEffect(() => {
const handleCallback = async () => {
if (state && code) {
console.log("OAuth Callback:", state, code);
await handleOAuthCallback();
}
};
handleCallback();
}, [state, code]);

if (user) {
window.location.href = ROUTES.HOME;
return null;
}
const { login, register, oauthLogin } = useAuth();
const router = useRouter();

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
Expand All @@ -107,7 +80,7 @@ export default function AuthPage() {
username: formData.name || "",
});
}
window.location.href = "/";
await router.navigate({ to: ROUTES.HOME });
} catch (error) {
console.error(`${mode} failed:`, error);
}
Expand All @@ -119,17 +92,6 @@ export default function AuthPage() {
window.location.href = resp.url;
};

const handleOAuthCallback = async () => {
if (state && code) {
const provider = state.split(":")[1];
console.log("OAuth Callback:", provider, code);
const data = await oauthCallback(provider.toLowerCase(), code);
console.log("OAuth Callback data:", data);
// TODO: redirect to previous page
window.location.href = "/";
}
};

return (
<div className="flex items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md">
Expand Down Expand Up @@ -220,21 +182,15 @@ export default function AuthPage() {
? "Don't have an account? "
: "Already have an account? "}
</span>
<a
href={isLogin ? ROUTES.SIGNUP : ROUTES.SIGNUP}
<Link
to={isLogin ? ROUTES.AUTH_REGISTER : ROUTES.AUTH_LOGIN}
className="text-primary hover:underline"
>
{isLogin ? "Sign up" : "Log in"}
</a>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}

createRoot(document.getElementById("root")!).render(
<App>
<AuthPage />
</App>,
);
5 changes: 3 additions & 2 deletions web/src/components/bookmarks-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/components/ui/card";
import type { Bookmark } from "@/lib/apis/bookmarks";
import { ROUTES } from "@/lib/router";
import { Link } from "@tanstack/react-router";

interface BookmarkListProps {
bookmarks: Bookmark[];
Expand All @@ -21,7 +22,7 @@ export default function BookmarkList({ bookmarks }: BookmarkListProps) {
key={bookmark.id}
className="overflow-hidden transition-transform transform hover:-translate-y-1 mx-2"
>
<a href={`${ROUTES.BOOKMARKS}?id=${bookmark.id}`} rel="noreferrer">
<Link to={ROUTES.BOOKMARK_DETAIL} params={{ id: bookmark.id }}>
{bookmark.metadata?.image && (
<img
src={bookmark.metadata.image}
Expand All @@ -45,7 +46,7 @@ export default function BookmarkList({ bookmarks }: BookmarkListProps) {
{bookmark.url}
</CardDescription>
</CardHeader>
</a>
</Link>
<CardContent>
<div className="flex flex-wrap gap-2">
{bookmark.metadata?.tags?.map((tag) => (
Expand Down
6 changes: 1 addition & 5 deletions web/src/components/bookmarks-sidebar-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ import {
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
import { ROUTES } from "@/lib/router";
import {
Bookmark,
ChevronRight,
Newspaper
} from "lucide-react";
import { Bookmark, ChevronRight, Newspaper } from "lucide-react";

const items = [
{
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,8 @@ import {
import { useBookmarkMutations, useBookmarks } from "@/lib/apis/bookmarks";
import { PlusCircle } from "lucide-react";
import { useState } from "react";
import { createRoot } from "react-dom/client";
import App from "./app-basic";

import ProtectedRoute from "@/components/protected-route";
import { useQueryState } from "nuqs";
import BookmarkDetailPage from "./bookmark-detail-page";

function BookmarksListView() {
export default function BookmarksListView() {
const { data: bookmarks = [] } = useBookmarks();
const { createBookmark } = useBookmarkMutations();
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -80,19 +74,3 @@ function BookmarksListView() {
</SidebarProvider>
);
}

function BookmarkPage() {
const [id] = useQueryState("id");
if (id != null) {
return <BookmarkDetailPage id={id} />;
}
return <BookmarksListView />;
}

createRoot(document.getElementById("root")!).render(
<App>
<ProtectedRoute>
<BookmarkPage />
</ProtectedRoute>
</App>,
);
2 changes: 1 addition & 1 deletion web/src/components/protected-route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
}

if (!user) {
window.location.href = ROUTES.LOGIN;
window.location.href = ROUTES.AUTH_LOGIN;
return null;
}

Expand Down
Loading

0 comments on commit 2035326

Please sign in to comment.