ArticlesModel as single point for storing articles and operations on them

This commit is contained in:
2025-11-20 15:38:10 +05:30
parent 2dfbdb950a
commit 0ed816e994
5 changed files with 93 additions and 25 deletions

View File

@@ -1,30 +1,21 @@
import React, { createContext, useState, useContext, useEffect } from 'react'; import React, { createContext, useState, useContext, useEffect } from 'react';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { ArticleModel } from '../types/models'; import {
ArticleModel,
ArticlesModel,
createArticlesModelObject,
} from '../types/models';
import { ArticleContextModel } from '../types/contexts'; import { ArticleContextModel } from '../types/contexts';
import { useAuth } from './Author'; import { useAuth } from './Author';
const ArticleContext = createContext<ArticleContextModel | undefined>(undefined); const ArticleContext = createContext<ArticleContextModel | undefined>(undefined);
export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [articles, setArticles] = useState<ArticleModel[]>([]); const [articles, setArticles] = useState<ArticlesModel>(createArticlesModelObject());
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { token, currentUser } = useAuth(); const { token, currentUser } = useAuth();
const upsertArticleInList = (updated: ArticleModel) => {
setArticles(prev => {
const exists = prev.some(a => a._id === updated._id);
if (exists) {
// UPDATE → replace item
return prev.map(a => (a._id === updated._id ? updated : a));
} else {
// CREATE → append to top
return [updated, ...prev];
}
});
};
/** 🔹 Author IDs must be strings for API, so we normalize here */ /** 🔹 Author IDs must be strings for API, so we normalize here */
const normalizeArticleForApi = (article: Partial<ArticleModel>) => { const normalizeArticleForApi = (article: Partial<ArticleModel>) => {
// Extract existing authors as a list of IDs (string[]) // Extract existing authors as a list of IDs (string[])
@@ -51,7 +42,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
const res = await api.get<ArticleModel[]>('/articles', { params: { skip: 0, limit: 100 } }); const res = await api.get<ArticleModel[]>('/articles', { params: { skip: 0, limit: 100 } });
const formatted = res.data.map((a) => ({ ...a, id: a._id || undefined })); const formatted = res.data.map((a) => ({ ...a, id: a._id || undefined }));
setArticles(formatted); setArticles(prev => prev.refresh(formatted));
} catch (err: any) { } catch (err: any) {
console.error('Failed to fetch articles:', err); console.error('Failed to fetch articles:', err);
setError(err.response?.data?.detail || 'Failed to fetch articles'); setError(err.response?.data?.detail || 'Failed to fetch articles');
@@ -77,7 +68,10 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
setError(null); setError(null);
const res = await api.put<ArticleModel>(`/articles/${articleData._id}`, normalizedArticleData); const res = await api.put<ArticleModel>(`/articles/${articleData._id}`, normalizedArticleData);
upsertArticleInList(res.data); setArticles(prev => {
prev.update(res.data);
return { ...prev };
});
return res.data; return res.data;
} catch (err: any) { } catch (err: any) {
console.error('Article update failed:', err); console.error('Article update failed:', err);
@@ -100,7 +94,10 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
setError(null); setError(null);
const res = await api.post<ArticleModel>(`/articles`, normalizedArticleData); const res = await api.post<ArticleModel>(`/articles`, normalizedArticleData);
upsertArticleInList(res.data); setArticles(prev => {
prev.create(res.data);
return { ...prev };
});
return res.data; return res.data;
} catch (err: any) { } catch (err: any) {
console.error('Article create failed:', err); console.error('Article create failed:', err);

View File

@@ -1,7 +1,11 @@
import { ArticleModel, AuthorModel } from "./models"; import {
ArticleModel,
ArticlesModel,
AuthorModel
} from "./models";
export interface ArticleContextModel { export interface ArticleContextModel {
articles: ArticleModel[]; articles: ArticlesModel;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
refreshArticles: () => Promise<void>; refreshArticles: () => Promise<void>;

View File

@@ -1,3 +1,9 @@
import {
createInList, readInList, updateInList, deleteInList,
createById, readById, updateById, deleteById
} from "../utils/articles";
export interface AuthorModel { export interface AuthorModel {
// meta fields // meta fields
_id?: string | null; _id?: string | null;
@@ -28,3 +34,62 @@ export interface ArticleModel {
// ref fields // ref fields
authors: AuthorModel[]; authors: AuthorModel[];
} }
export interface ArticlesModel {
articles: ArticleModel[];
articlesById: Record<string, ArticleModel>;
// articlesByTag: Record<string, ArticleModel[]>;
// articlesByAuthor: Record<string, ArticleModel[]>;
refresh(list: ArticleModel[]): ArticlesModel;
create(a: ArticleModel): ArticlesModel;
readByIndex(index: number): ArticleModel | undefined;
readById(id: string): ArticleModel | undefined;
update(a: ArticleModel): ArticlesModel;
delete(id: string): ArticlesModel;
}
// ---------- FACTORY ----------
export function createArticlesModelObject(): ArticlesModel {
return {
articles: [],
articlesById: {},
refresh(list) {
this.articles = list;
const map: Record<string, ArticleModel> = {};
for (const a of list) {
if (a._id) map[a._id] = a;
}
this.articlesById = map;
return this;
},
create(a) {
this.articles = createInList(this.articles, a);
this.articlesById = createById(this.articlesById, a);
return this;
},
readByIndex(index) {
return readInList(this.articles, index);
},
readById(id) {
return readById(this.articlesById, id);
},
update(a) {
this.articles = updateInList(this.articles, a);
this.articlesById = updateById(this.articlesById, a);
return this;
},
delete(id) {
this.articles = deleteInList(this.articles, id);
this.articlesById = deleteById(this.articlesById, id);
return this;
}
};
}

View File

@@ -1,7 +1,10 @@
import { ArticleModel } from "./models"; import {
ArticleModel,
ArticlesModel,
} from "./models";
export interface LatestProps { export interface LatestProps {
articles: ArticleModel[]; articles: ArticlesModel;
onSelectArticle?: (index: number) => void; onSelectArticle?: (index: number) => void;
onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback
} }
@@ -12,7 +15,7 @@ export interface LoginProps {
} }
export interface MainContentProps { export interface MainContentProps {
articles: ArticleModel[]; articles: ArticlesModel;
onSelectArticle: (index: number) => void; onSelectArticle: (index: number) => void;
} }
@@ -49,7 +52,7 @@ export interface ArticleCardProps {
} }
export interface ArticleGridProps { export interface ArticleGridProps {
articles: ArticleModel[]; articles: ArticlesModel;
onSelectArticle: (index: number) => void; onSelectArticle: (index: number) => void;
xs?: number; // default 12 for mobile full-width xs?: number; // default 12 for mobile full-width
md12?: number, // default 12 (full-width) md12?: number, // default 12 (full-width)

View File

@@ -1,6 +1,5 @@
import { import {
ArticleModel, ArticleModel,
ArticleRepoModel
} from "../types/models"; } from "../types/models";
// List helpers // List helpers