From 42fe31fc69a4057d5903d4991cefb26343a87722 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 15:35:28 +0530 Subject: [PATCH] refactor(types): centralize all interfaces into dedicated type models and update context usage - Moved all interface definitions into - Updated all providers and components to import interfaces from types/ folder - Renamed interfaces for clarity - Updated Article component to use typed props interface - Added descriptive inline date formatting utility examples --- src/blog/components/Article.tsx | 20 +++--- src/blog/components/Latest.tsx | 9 +-- src/blog/providers/Article.tsx | 40 ++---------- src/blog/providers/Author.tsx | 107 ++++++++++++++++++++++++++++++++ src/blog/types/contexts.ts | 19 ++++++ src/blog/types/models.ts | 30 +++++++++ src/blog/types/props.ts | 12 ++++ 7 files changed, 186 insertions(+), 51 deletions(-) create mode 100644 src/blog/providers/Author.tsx create mode 100644 src/blog/types/contexts.ts create mode 100644 src/blog/types/models.ts create mode 100644 src/blog/types/props.ts diff --git a/src/blog/components/Article.tsx b/src/blog/components/Article.tsx index 99d718c..429fd81 100644 --- a/src/blog/components/Article.tsx +++ b/src/blog/components/Article.tsx @@ -3,6 +3,7 @@ import { marked } from 'marked'; import { Box, Typography, Avatar, Divider, IconButton, Chip } from '@mui/material'; import { styled } from '@mui/material/styles'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; +import { ArticleProps } from '../types/props'; const ArticleContainer = styled(Box)(({ theme }) => ({ maxWidth: '800px', @@ -23,11 +24,9 @@ const CoverImage = styled('img')({ export default function Article({ article, - onBack, -}: { - article: any; - onBack: () => void; -}) { + onBack +}: ArticleProps) { + return ( @@ -44,16 +43,19 @@ export default function Article({ {article.title} - - {article.subtitle} - {article.authors[0].name} - {article.authors[0].date} + {new Date(article.created_at).toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} diff --git a/src/blog/components/Latest.tsx b/src/blog/components/Latest.tsx index 6ed7682..8893b8f 100644 --- a/src/blog/components/Latest.tsx +++ b/src/blog/components/Latest.tsx @@ -7,7 +7,7 @@ import Typography from '@mui/material/Typography'; import { styled } from '@mui/material/styles'; import NavigateNextRoundedIcon from '@mui/icons-material/NavigateNextRounded'; import CircularProgress from '@mui/material/CircularProgress'; -import type { Article } from '../providers/Article'; // ✅ import type for correctness +import { LatestProps } from "../types/props"; import Fade from '@mui/material/Fade'; // ✅ for smooth appearance @@ -89,13 +89,6 @@ function Author({ authors }: { authors: { name: string; avatar: string }[] }) { ); } -// ---- Latest component ---- // -interface LatestProps { - articles: Article[]; - onSelectArticle?: (index: number) => void; - onLoadMore?: (offset: number, limit: number) => Promise; // optional async callback -} - export default function Latest({ articles, onSelectArticle, onLoadMore }: LatestProps) { const [visibleCount, setVisibleCount] = React.useState(2); const [loadingMore, setLoadingMore] = React.useState(false); diff --git a/src/blog/providers/Article.tsx b/src/blog/providers/Article.tsx index a85b425..4ecc5ef 100644 --- a/src/blog/providers/Article.tsx +++ b/src/blog/providers/Article.tsx @@ -1,42 +1,14 @@ import React, { createContext, useState, useContext, useEffect } from 'react'; import axios from 'axios'; +import { ArticleModel } from "../types/models"; +import { ArticleContextModel } from "../types/contexts"; -interface Author { - _id?: string | null; - username: string; - name: string; - email: string; - avatar: string; - is_active: boolean; - created_at?: string; - updated_at?: string; -} - -export interface Article { - _id?: string | null; - created_at: string; - updated_at: string; - img: string; - tag: string; - title: string; - description: string; - content: string; - authors: Author[]; -} - -interface ArticleContextType { - articles: Article[]; - loading: boolean; - error: string | null; - refreshArticles: () => Promise; -} - -const ArticleContext = createContext(undefined); +const ArticleContext = createContext(undefined); const API_BASE = import.meta.env.VITE_API_BASE_URL; export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [articles, setArticles] = useState([]); + const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -46,7 +18,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child setError(null); // ✅ Use correct full endpoint from OpenAPI spec - const res = await axios.get(`${API_BASE}/articles`, { + const res = await axios.get(`${API_BASE}/articles`, { params: { skip: 0, limit: 10 }, }); @@ -76,7 +48,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child ); }; -export const useArticles = (): ArticleContextType => { +export const useArticles = (): ArticleContextModel => { const ctx = useContext(ArticleContext); if (!ctx) throw new Error('useArticles must be used inside ArticleProvider'); return ctx; diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx new file mode 100644 index 0000000..4cd8ac6 --- /dev/null +++ b/src/blog/providers/Author.tsx @@ -0,0 +1,107 @@ +import React, { createContext, useState, useEffect, useContext } from 'react'; +import axios from 'axios'; +import { AuthorModel } from "../types/models"; +import { AuthContextModel } from "../types/contexts"; + +const AuthContext = createContext(undefined); + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [currentUser, setCurrentUser] = useState(null); + const [authors, setAuthors] = useState([]); + const [token, setToken] = useState(localStorage.getItem('token')); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + /** 🔹 Login and store JWT token */ + const login = async (email: string, password: string) => { + try { + setLoading(true); + setError(null); + + const res = await axios.post(`${API_BASE}/auth/login`, { email, password }); + const { access_token, user } = res.data; + + if (access_token) { + localStorage.setItem('token', access_token); + setToken(access_token); + setCurrentUser(user); + } + } catch (err: any) { + console.error('Login failed:', err); + setError(err.response?.data?.detail || 'Invalid credentials'); + } finally { + setLoading(false); + } + }; + + /** 🔹 Logout and clear everything */ + const logout = () => { + localStorage.removeItem('token'); + setToken(null); + setCurrentUser(null); + setAuthors([]); + }; + + /** 🔹 Fetch all authors (requires valid JWT) */ + const refreshAuthors = async () => { + if (!token) return; + try { + setLoading(true); + setError(null); + + const res = await axios.get(`${API_BASE}/authors`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + setAuthors(res.data); + } catch (err: any) { + console.error('Failed to fetch authors:', err); + setError(err.message || 'Failed to fetch authors'); + } finally { + setLoading(false); + } + }; + + /** 🔹 Auto-load current user if token exists */ + const fetchCurrentUser = async () => { + if (!token) return; + try { + const res = await axios.get(`${API_BASE}/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setCurrentUser(res.data); + } catch (err: any) { + console.error('Failed to fetch current user:', err); + logout(); // invalid/expired token + } + }; + + useEffect(() => { + if (token) fetchCurrentUser(); + }, [token]); + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextModel => { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error('useAuth must be used inside AuthProvider'); + return ctx; +}; diff --git a/src/blog/types/contexts.ts b/src/blog/types/contexts.ts new file mode 100644 index 0000000..2f12ed6 --- /dev/null +++ b/src/blog/types/contexts.ts @@ -0,0 +1,19 @@ +import { ArticleModel, AuthorModel } from "./models"; + +export interface ArticleContextModel { + articles: ArticleModel[]; + loading: boolean; + error: string | null; + refreshArticles: () => Promise; +} + +export interface AuthContextModel { + currentUser: AuthorModel | null; + authors: AuthorModel[]; + token: string | null; + loading: boolean; + error: string | null; + login: (email: string, password: string) => Promise; + logout: () => void; + refreshAuthors: () => Promise; +} diff --git a/src/blog/types/models.ts b/src/blog/types/models.ts new file mode 100644 index 0000000..ab50a10 --- /dev/null +++ b/src/blog/types/models.ts @@ -0,0 +1,30 @@ +export interface AuthorModel { + // meta fields + _id?: string | null; + created_at: string; + updated_at: string; + + // model fields + username: string; + name: string; + email: string; + avatar: string; + is_active: boolean; +} + +export interface ArticleModel { + // meta fields + _id?: string | null; + created_at: string; + updated_at: string; + + // model fields + img: string; + tag: string; + title: string; + description: string; + content: string; + + // ref fields + authors: AuthorModel[]; +} diff --git a/src/blog/types/props.ts b/src/blog/types/props.ts new file mode 100644 index 0000000..91cb30c --- /dev/null +++ b/src/blog/types/props.ts @@ -0,0 +1,12 @@ +import { ArticleModel } from "./models"; + +export interface LatestProps { + articles: ArticleModel[]; + onSelectArticle?: (index: number) => void; + onLoadMore?: (offset: number, limit: number) => Promise; // optional async callback +} + +export interface ArticleProps { + article: ArticleModel; + onBack: () => void; +}