Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9b90f6d3e |
-27
@@ -1,28 +1 @@
|
|||||||
/backups
|
/backups
|
||||||
|
|
||||||
# Python
|
|
||||||
/__pycache__/*
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Alembic
|
|
||||||
|
|
||||||
Alembic allows for our database migrations to be tracked in a version control system.
|
|
||||||
|
|
||||||
To create a new migration run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
alembic revision --autogenerate -m 'Describe change here'
|
|
||||||
```
|
|
||||||
|
|
||||||
It's best practice to review the script post revision creation: `alembic/versions`
|
|
||||||
|
|
||||||
To apply the migration:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
alembic upgrade head
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can re-check using `alembic check`
|
|
||||||
|
|
||||||
If we need to rollback use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
alembic downgrade -i
|
|
||||||
```
|
|
||||||
Binary file not shown.
@@ -1,58 +0,0 @@
|
|||||||
"""1.1.0
|
|
||||||
|
|
||||||
Revision ID: b9dcd098debd
|
|
||||||
Revises: a3ac646e53a8
|
|
||||||
Create Date: 2025-06-14 09:22:14.878105
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'b9dcd098debd'
|
|
||||||
down_revision: Union[str, None] = 'a3ac646e53a8'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
|
||||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
|
||||||
op.drop_index(op.f('ix_users_username'), table_name='users')
|
|
||||||
op.drop_table('users')
|
|
||||||
op.drop_index(op.f('ix_items_id'), table_name='items')
|
|
||||||
op.drop_index(op.f('ix_items_name'), table_name='items')
|
|
||||||
op.drop_table('items')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('items',
|
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('body', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('items_pkey'))
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False)
|
|
||||||
op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False)
|
|
||||||
op.create_table('users',
|
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('permissions', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('subscriber', sa.BOOLEAN(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('users_pkey'))
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
|
|
||||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-7
@@ -31,10 +31,10 @@ def delete_item(db: Session, item_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def authenticate_user(db: Session, username: str, password: str):
|
def authenticate_user(db: Session, username: str, password: str):
|
||||||
user = get_user_by_username(db, username) or get_user_by_email(db, username)
|
user = get_user_by_username(db, username)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
if not verify_password(password, user.hashed_password):
|
if not verify_password(password, str(user.hashed_password)):
|
||||||
return None
|
return None
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -50,11 +50,7 @@ def get_user_by_email(db: Session, email: str):
|
|||||||
def create_user(db: Session, user: schemas.UserCreate):
|
def create_user(db: Session, user: schemas.UserCreate):
|
||||||
hashed_pw = hash_password(user.password)
|
hashed_pw = hash_password(user.password)
|
||||||
db_user = models.User(
|
db_user = models.User(
|
||||||
username=user.username,
|
username=user.username, email=user.email, hashed_password=hashed_pw
|
||||||
email=user.email,
|
|
||||||
hashed_password=hashed_pw,
|
|
||||||
permissions=user.permissions,
|
|
||||||
subscriber=user.subscriber,
|
|
||||||
)
|
)
|
||||||
db.add(db_user)
|
db.add(db_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from fastapi import FastAPI, Depends, HTTPException, status
|
from fastapi import FastAPI, Depends, HTTPException, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -11,14 +10,6 @@ Base.metadata.create_all(bind=engine)
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["http://localhost:3000", "https://localhost:3000"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Dependency
|
# Dependency
|
||||||
def get_db():
|
def get_db():
|
||||||
@@ -90,5 +81,4 @@ def register_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
|||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Account with that email already registered",
|
detail="Account with that email already registered",
|
||||||
)
|
)
|
||||||
# Default Cases
|
|
||||||
return crud.create_user(db, user)
|
return crud.create_user(db, user)
|
||||||
|
|||||||
@@ -24,15 +24,13 @@ class UserBase(BaseModel):
|
|||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
password: str
|
password: str
|
||||||
permissions: dict = {}
|
|
||||||
subscriber: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class UserOut(UserBase):
|
class UserOut(UserBase):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
# Other Schemas
|
# Other Schemas
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
@@ -5,8 +6,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from app.logger_config import Logger
|
from app.logger_config import Logger
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemas=["bcrypt"], deprecated="auto")
|
||||||
_logger = Logger().logger
|
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
@@ -37,5 +37,5 @@ def decode_access_token(token: str) -> Mapping[Any, Any] | None:
|
|||||||
try:
|
try:
|
||||||
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
except JWTError:
|
except JWTError:
|
||||||
_logger.exception(msg="Failed to Decode JWT", extra={"TOKEN": token})
|
logging.exception(msg="Failed to Decode JWT", extra={"TOKEN": token})
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"fastapi[standard]>=0.115.12",
|
"fastapi[standard]>=0.115.12",
|
||||||
"passlib[bcrypt]>=1.7.4",
|
"passlib[bcrypt]>=1.7.4",
|
||||||
"psycopg2-binary>=2.9.10",
|
"psycopg2-binary>=2.9.10",
|
||||||
|
"pyrefly>=0.18.1",
|
||||||
"python-jose[cryptography]>=3.5.0",
|
"python-jose[cryptography]>=3.5.0",
|
||||||
"sqlalchemy>=2.0.41",
|
"sqlalchemy>=2.0.41",
|
||||||
"uvicorn[standard]>=0.34.2",
|
"uvicorn[standard]>=0.34.2",
|
||||||
|
|||||||
Generated
-17
@@ -12,7 +12,6 @@
|
|||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.6.0"
|
"react-router-dom": "^7.6.0"
|
||||||
},
|
},
|
||||||
@@ -4393,22 +4392,6 @@
|
|||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hook-form": {
|
|
||||||
"version": "7.57.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
|
|
||||||
"integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/react-hook-form"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-markdown": {
|
"node_modules/react-markdown": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.6.0"
|
"react-router-dom": "^7.6.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+59
-380
@@ -3,191 +3,78 @@ import {
|
|||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
Link,
|
Link,
|
||||||
useNavigate,
|
useParams,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { useState, useContext, useEffect, createContext, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { BlogViewer } from "./utils/BlogViewer";
|
import { BlogViewer } from "./utils/BlogViewer";
|
||||||
import { BlogList } from "./utils/BlogList";
|
import { BlogList } from "./utils/BlogList";
|
||||||
import { RequireAdmin } from "./utils/RouteGuard";
|
import { RequireAdmin } from "./utils/RouteGuard";
|
||||||
import { AdminPage } from "./utils/AdminPage";
|
import { AdminPage } from "./utils/AdminPage";
|
||||||
import Unauthorized from "./utils/UnauthorizedPage";
|
import Unauthorized from "./utils/UnauthorizedPage";
|
||||||
|
|
||||||
// Use Vite environment variable for API base URL
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
|
|
||||||
|
|
||||||
// Auth Context
|
|
||||||
interface AuthContextProps {
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
login: (token: string) => void;
|
|
||||||
logout: () => void;
|
|
||||||
}
|
|
||||||
const AuthContext = createContext<AuthContextProps>({
|
|
||||||
isAuthenticated: false,
|
|
||||||
login: () => {},
|
|
||||||
logout: () => {},
|
|
||||||
});
|
|
||||||
export const useAuth = () => useContext(AuthContext);
|
|
||||||
|
|
||||||
const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
setIsAuthenticated(!!token);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const login = (token: string) => {
|
|
||||||
localStorage.setItem("token", token);
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
navigate("/signin");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [darkMode, setDarkMode] = useState(true);
|
const [darkMode, setDarkMode] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<div className={darkMode ? "dark" : ""}>
|
||||||
<AuthProvider>
|
<Router>
|
||||||
<div className={darkMode ? "dark" : ""}>
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
<AppBar toggleDarkMode={() => setDarkMode(!darkMode)} />
|
||||||
<AppBar toggleDarkMode={() => setDarkMode(!darkMode)} />
|
<div className="p-4">
|
||||||
<div className="p-4">
|
<Routes>
|
||||||
<Routes>
|
<Route path="/" element={<LandingPage />} />
|
||||||
<Route path="/" element={<LandingPage />} />
|
<Route path="/blog" element={<BlogList />} />
|
||||||
<Route path="/blog" element={<BlogList />} />
|
<Route path="/blog/:slug" element={<BlogViewer />} />
|
||||||
<Route path="/blog/:slug" element={<BlogViewer />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/contact" element={<Contact />} />
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route
|
||||||
<Route
|
path="/admin"
|
||||||
path="/admin"
|
element={
|
||||||
element={
|
<RequireAdmin>
|
||||||
<RequireAdmin>
|
<AdminPage />
|
||||||
<AdminPage />
|
</RequireAdmin>
|
||||||
</RequireAdmin>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route path="/unauthorized" element={<Unauthorized />} />
|
||||||
<Route path="/unauthorized" element={<Unauthorized />} />
|
</Routes>
|
||||||
<Route path="/register" element={<Register />} />
|
|
||||||
<Route path="/signin" element={<SignIn />} />
|
|
||||||
<Route path="/profile" element={<Profile />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AuthProvider>
|
</Router>
|
||||||
</Router>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppBar({ toggleDarkMode }: { toggleDarkMode: () => void }) {
|
function AppBar({ toggleDarkMode }: { toggleDarkMode: () => void }) {
|
||||||
const { isAuthenticated, logout } = useAuth();
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
menuOpen &&
|
|
||||||
menuRef.current &&
|
|
||||||
!menuRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setMenuOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [menuOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-white dark:bg-gray-800 shadow p-4 flex items-center relative">
|
<nav className="bg-white dark:bg-gray-800 shadow p-4 flex space-x-4 items-center">
|
||||||
<Link to="/" className="font-bold text-blue-600 dark:text-blue-400">
|
<Link to="/" className="font-bold text-blue-600 dark:text-blue-400">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/blog"
|
to="/blog"
|
||||||
className="ml-4 text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
className="text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/about"
|
to="/about"
|
||||||
className="ml-4 text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
className="text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
About Me
|
About Me
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="ml-4 text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
className="text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
Contact
|
Contact
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ml-auto flex items-center space-x-4">
|
<button
|
||||||
{!isAuthenticated ? (
|
onClick={toggleDarkMode}
|
||||||
<>
|
className="ml-auto text-sm bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded"
|
||||||
<Link
|
>
|
||||||
to="/register"
|
Toggle Dark Mode
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
|
</button>
|
||||||
>
|
|
||||||
Register
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/signin"
|
|
||||||
className="px-3 py-1 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="relative" ref={menuRef}>
|
|
||||||
<button
|
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
|
||||||
className="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<span className="text-gray-700 dark:text-gray-200">U</span>
|
|
||||||
</button>
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-32 bg-white dark:bg-gray-700 shadow rounded">
|
|
||||||
<Link
|
|
||||||
to="/profile"
|
|
||||||
className="block px-4 py-2 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="w-full text-left block px-4 py-2 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={toggleDarkMode}
|
|
||||||
className="text-sm bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded"
|
|
||||||
>
|
|
||||||
Toggle Dark Mode
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -203,249 +90,41 @@ function LandingPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile placeholder
|
export const BlogPost = () => {
|
||||||
function Profile() {
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const [content, setContent] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!slug) return;
|
||||||
|
|
||||||
|
fetch(`../public/blogs/${slug}.md`)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => setContent(text));
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (!content) return <div>Loading...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-10">
|
<article className="prose dark:prose-invert max-w-none p-4">
|
||||||
<h2 className="text-2xl font-bold mb-4">Profile</h2>
|
<ReactMarkdown>{content}</ReactMarkdown>
|
||||||
<p>Profile page coming soon.</p>
|
</article>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-10">
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">About Me</h2>
|
<h2 className="text-2xl font-bold mb-4">About Me</h2>
|
||||||
<p>About page content coming soon.</p>
|
<p>This is a placeholder for information about me.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Contact() {
|
function Contact() {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-10">
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Contact</h2>
|
<h2 className="text-2xl font-bold mb-4">Contact</h2>
|
||||||
<p>Contact page content coming soon.</p>
|
<p>This is a placeholder for contact information or a form.</p>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration form with API integration
|
|
||||||
interface RegisterForm {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Register() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<RegisterForm>();
|
|
||||||
|
|
||||||
const onSubmit = async (data: RegisterForm) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/register`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: data.username,
|
|
||||||
email: data.email,
|
|
||||||
password: data.password,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const err = await response.json();
|
|
||||||
throw new Error(err.detail || "Registration failed");
|
|
||||||
}
|
|
||||||
navigate("/signin");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-md mx-auto py-10">
|
|
||||||
<h2 className="text-2xl font-bold mb-6 text-center">Register</h2>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="username" className="block text-sm font-medium mb-1">
|
|
||||||
Username
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
{...register("username", { required: true })}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.username && (
|
|
||||||
<span className="text-red-600 text-sm">Username is required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
{...register("email", {
|
|
||||||
required: true,
|
|
||||||
pattern: /^\S+@\S+\.\S+$/,
|
|
||||||
})}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<span className="text-red-600 text-sm">
|
|
||||||
Valid email is required
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
{...register("password", {
|
|
||||||
required: true,
|
|
||||||
pattern: /(?=.*[A-Z])(?=.*\d).+/,
|
|
||||||
})}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<span className="text-red-600 text-sm">
|
|
||||||
Password must include uppercase and number
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
{...register("confirmPassword", {
|
|
||||||
validate: (value) =>
|
|
||||||
value === watch("password") || "Passwords do not match",
|
|
||||||
})}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.confirmPassword && (
|
|
||||||
<span className="text-red-600 text-sm">
|
|
||||||
{errors.confirmPassword.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign-in form with API integration
|
|
||||||
interface SignInForm {
|
|
||||||
identifier: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignIn() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<SignInForm>();
|
|
||||||
const { login } = useAuth();
|
|
||||||
|
|
||||||
const onSubmit = async (data: SignInForm) => {
|
|
||||||
try {
|
|
||||||
const body = new URLSearchParams();
|
|
||||||
body.append("username", data.identifier);
|
|
||||||
body.append("password", data.password);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/login`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const err = await response.json();
|
|
||||||
throw new Error(err.detail || "Login failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await response.json();
|
|
||||||
login(tokenData.access_token);
|
|
||||||
navigate("/");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-md mx-auto py-10">
|
|
||||||
<h2 className="text-2xl font-bold mb-6 text-center">Sign In</h2>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="identifier"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Username or Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="identifier"
|
|
||||||
{...register("identifier", { required: true })}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.identifier && (
|
|
||||||
<span className="text-red-600 text-sm">This field is required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="signinPassword"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="signinPassword"
|
|
||||||
type="password"
|
|
||||||
{...register("password", { required: true })}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<span className="text-red-600 text-sm">Password is required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user