Basic MUI implemented

This commit is contained in:
stephen 2026-01-06 20:24:25 +11:00
parent 5e10df93b1
commit 65b3d4baa4
16 changed files with 922 additions and 263 deletions

14
errors.txt Normal file
View file

@ -0,0 +1,14 @@
> 124_webcalibre2@0.1.0 dev
> next dev
▲ Next.js 16.1.1 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.100:3000
- Environments: .env.local, .env
✓ Starting...
✓ Ready in 810ms
GET / 500 in 7.5s (compile: 127ms, proxy.ts: 128ms, render: 7.3s)
GET / 500 in 6.9s (compile: 1654µs, proxy.ts: 2ms, render: 6.9s)
[?25h

672
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,12 @@
"lint": "eslint"
},
"dependencies": {
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.6",
"@mui/material": "^7.3.6",
"@mui/material-nextjs": "^7.3.6",
"next": "16.1.1",
"next-auth": "^5.0.0-beta.30",
"react": "19.2.3",

View file

@ -1,41 +1,32 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
import ThemeRegistry from '@/components/ThemeRegistry';
import Navbar from '@/components/layout/Navbar';
import { auth } from "@/auth";
import "./globals.css";
import Navbar from "@/components/navbar";
import { auth } from "@/auth";
import { SessionProvider } from "next-auth/react";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
export const metadata = {
title: "WebCalibre 2",
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
// Fetch the session server-side to prevent UI flickering
const session = await auth();
return (
<SessionProvider session={session}>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Navbar />
<main className="p-5">{children}</main>
</body>
</html>
</SessionProvider>
<html lang="en" suppressHydrationWarning>
<body>
<InitColorSchemeScript attribute="class" />
{/* ThemeRegistry handles the MUI Theme and Cache Provider */}
<ThemeRegistry>
{/* We pass the user object to the Navbar so it can show the profile pic */}
<Navbar user={session?.user} />
<main style={{ minHeight: '100vh' }}>
{children}
</main>
</ThemeRegistry>
</body>
</html>
);
}
}

View file

@ -1,20 +0,0 @@
import LoginButton from "@/components/LoginButton";
function Login(){
return (
<div className="flex items-center justify-center min-h-[90vh]">
<div className="p-5 border rounded-md max-w-[50%] flex gap-5 flex-col">
<h1 className="text-center font-semibold text-xl">Login</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus
reprehenderit aut vitae consequuntur deleniti itaque totam eaque
perspiciatis nulla magnam officiis blanditiis enim fuga perferendis
deserunt, praesentium vel. Omnis, dolorum?
</p>
<LoginButton />
</div>
</div>
);
}
export default Login

View file

@ -1,66 +1,15 @@
import Image from "next/image";
import styles from "./page.module.css";
// src/app/page.tsx
import { Container, Typography, Button, Box } from '@mui/material';
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className={styles.intro}>
<h1>To get started, edit the page.tsx file.</h1>
<p>
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className={styles.secondary}
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
<Container maxWidth="md">
<Box sx={{ my: 4, textAlign: 'center' }}>
<Typography variant="h2" component="h1" gutterBottom>
WebCalibre 2
</Typography>
<Button variant="contained">Explore Library</Button>
</Box>
</Container>
);
}
}

View file

@ -1,24 +0,0 @@
"use client";
import { signIn } from "next-auth/react";
import { useState } from "react";
function LoginButton() {
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleLogin = async () => {
setIsLoading(true);
await signIn("microsoft-entra-id");
setIsLoading(false);
};
return (
<button
onClick={() => handleLogin()}
className="w-full bg-blue-800 text-white my-5 p-3 rounded-md hover:opacity-80 disabled:opacity-75"
disabled={isLoading}
>
{isLoading ? "Signing in..." : "Login With Microsoft"}
</button>
);
}
export default LoginButton;

View file

@ -1,15 +0,0 @@
"use client";
import { signOut } from "next-auth/react";
function LogoutButton() {
return (
<button
onClick={() => signOut()}
className="w-full bg-blue-800 text-white p-3 rounded-md hover:opacity-80"
>
Logout
</button>
);
}
export default LogoutButton;

View file

@ -0,0 +1,17 @@
'use client';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from '@/lib/theme';
export default function ThemeRegistry({ children }: { children: React.ReactNode }) {
return (
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</AppRouterCacheProvider>
);
}

View file

@ -0,0 +1,22 @@
// src/components/layout/AppShell.tsx
'use client';
import React from 'react';
import { AppBar, Box, Toolbar } from '@mui/material';
import Navbar from './Navbar';
const AppShell = ({ children }: { children: React.ReactNode }) => {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="fixed">
<Toolbar>
<Navbar />
</Toolbar>
</AppBar>
<Box component="main" sx={{ flexGrow: 1, p: 3, mt: 8 }}>
{children} {/* This replaces <Outlet /> */}
</Box>
</Box>
);
};

View file

@ -0,0 +1,87 @@
'use client';
import React from 'react';
import { AppBar, Toolbar, Typography, Button, Box, Container, Avatar, IconButton } from '@mui/material';
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
import MenuIcon from '@mui/icons-material/Menu';
import Link from 'next/link';
import { signIn, signOut } from "next-auth/react";
interface NavbarProps {
user?: {
name?: string | null;
email?: string | null;
image?: string | null;
};
}
export default function Navbar({ user }: NavbarProps) {
const isLoggedIn = !!user;
return (
<AppBar position="sticky" elevation={0} sx={{ backgroundColor: 'white', color: 'text.primary', borderBottom: '1px solid #e0e0e0' }}>
<Container maxWidth="lg">
<Toolbar disableGutters>
{/* Menu Icon for the future AppDrawer */}
<IconButton edge="start" color="inherit" aria-label="menu" sx={{ mr: 2 }}>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
component={Link}
href="/"
sx={{ fontWeight: 700, color: 'primary.main', textDecoration: 'none', flexGrow: 1 }}
>
WebCalibre 2
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{isLoggedIn ? (
<>
{/* User Info: Name and Small Email */}
<Box sx={{ textAlign: 'right', display: { xs: 'none', sm: 'block' } }}>
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.2 }}>
{user.name}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block' }}>
{user.email}
</Typography>
</Box>
{/* Profile Picture */}
<Avatar
src={user.image || ""}
sx={{ width: 38, height: 38, border: '1px solid #eee' }}
>
{user.name?.charAt(0)}
</Avatar>
<Button
variant="outlined"
color="inherit"
size="small"
// We are still calling the function, just giving it a destination!
onClick={() => signOut({ callbackUrl: '/' })}
sx={{ borderRadius: '8px', textTransform: 'none' }}
>
Logout
</Button>
</>
) : (
// Login button triggers the Microsoft flow directly
<Button
variant="contained"
disableElevation
onClick={() => signIn("microsoft-entra-id", { callbackUrl: "/dashboard" })}
sx={{ borderRadius: '8px', textTransform: 'none', px: 3 }}
>
Login
</Button>
)}
</Box>
</Toolbar>
</Container>
</AppBar>
);
}

View file

@ -1,33 +0,0 @@
import { auth } from "@/auth";
import Link from "next/link";
async function Navbar() {
const session = await auth();
return (
<nav className="flex items-center justify-between gap-4 px-5 py-2">
<div>
<Link href={"/"} className="font-bold">
AppLogo
</Link>
</div>
{session?.user?.email ? (
<ul className="flex items-center gap-4 flex-row">
<li>
<Link href={"/dashboard"}>Dashboard</Link>
</li>
<li>
<Link href={"/profile"}>Profile</Link>
</li>
</ul>
) : (
<ul className="flex items-center gap-4 flex-row">
<li>
<Link href={"/login"}>Login</Link>
</li>
</ul>
)}
</nav>
);
}
export default Navbar;

View file

@ -0,0 +1,15 @@
'use client';
import { SessionProvider } from "next-auth/react";
// ... other imports like ThemeProvider
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</AppRouterCacheProvider>
</SessionProvider>
);
}

58
src/lib/theme.ts Normal file
View file

@ -0,0 +1,58 @@
import { createTheme } from '@mui/material/styles';
import { Roboto } from 'next/font/google';
// Load the font optimized for Next.js
const roboto = Roboto({
weight: ['300', '400', '500', '700'],
subsets: ['latin'],
display: 'swap',
});
const theme = createTheme({
// 1. Color Palette (Clean & Professional for a Library App)
palette: {
mode: 'light',
primary: {
main: '#1976d2', // Professional Blue
},
secondary: {
main: '#9c27b0', // Purple for accents
},
background: {
default: '#f4f6f8', // Light grey for the app background
paper: '#ffffff',
},
},
// 2. Typography
typography: {
fontFamily: roboto.style.fontFamily,
h6: {
fontWeight: 600,
},
},
// 3. Component Defaults
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none', // Prevents all-caps buttons
borderRadius: 8,
},
},
},
MuiPaper: {
defaultProps: {
elevation: 2,
},
styleOverrides: {
root: {
borderRadius: 12,
},
},
},
},
});
export default theme;

View file

@ -1,40 +0,0 @@
import { NextResponse } from "next/server";
import {auth } from "@/auth";
//export { auth as middleware } from "@/auth";
const protectedRoutes=["/dashboard","/profile"]
const authPageRoutes=["/login"]
const apiAuthPrefix ="/api/auth"
export default auth( (req) =>{
const { nextUrl } = req;
//const authData = await req.auth;
const isLoggedIn = !!req.auth;
const path = nextUrl.pathname
const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix);
const isProtectedRoute = protectedRoutes.includes(path);
const isAuthPageRoute = authPageRoutes.includes(path);
// console.log({ authData });
if(isApiAuthRoute){
return NextResponse.next();
}
if(isProtectedRoute && !isLoggedIn){
return NextResponse.redirect(new URL("/login", req.nextUrl));
}
if(isLoggedIn && isAuthPageRoute){
return NextResponse.redirect(new URL("/dashboard",req.nextUrl))
}
return NextResponse.next();
})
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

30
src/proxy.ts Normal file
View file

@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import { auth } from "@/auth";
const protectedRoutes = ["/dashboard", "/profile"];
const apiAuthPrefix = "/api/auth";
export const proxy = auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
const path = nextUrl.pathname;
const isApiAuthRoute = path.startsWith(apiAuthPrefix);
const isProtectedRoute = protectedRoutes.includes(path);
// 1. Allow API Auth calls (Login/Logout/Callback)
if (isApiAuthRoute) {
return NextResponse.next();
}
// 2. CHANGED: Redirect to HOME (/) instead of /login if logged out
if (isProtectedRoute && !isLoggedIn) {
return NextResponse.redirect(new URL("/", nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};