ArticlesModel as single point for storing articles and operations on them
This commit is contained in:
@@ -1,30 +1,21 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
import { api } from '../utils/api';
|
||||
import { ArticleModel } from '../types/models';
|
||||
import {
|
||||
ArticleModel,
|
||||
ArticlesModel,
|
||||
createArticlesModelObject,
|
||||
} from '../types/models';
|
||||
import { ArticleContextModel } from '../types/contexts';
|
||||
import { useAuth } from './Author';
|
||||
|
||||
const ArticleContext = createContext<ArticleContextModel | undefined>(undefined);
|
||||
|
||||
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 [error, setError] = useState<string | null>(null);
|
||||
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 */
|
||||
const normalizeArticleForApi = (article: Partial<ArticleModel>) => {
|
||||
// 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 formatted = res.data.map((a) => ({ ...a, id: a._id || undefined }));
|
||||
setArticles(formatted);
|
||||
setArticles(prev => prev.refresh(formatted));
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch articles:', err);
|
||||
setError(err.response?.data?.detail || 'Failed to fetch articles');
|
||||
@@ -77,7 +68,10 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||
setError(null);
|
||||
|
||||
const res = await api.put<ArticleModel>(`/articles/${articleData._id}`, normalizedArticleData);
|
||||
upsertArticleInList(res.data);
|
||||
setArticles(prev => {
|
||||
prev.update(res.data);
|
||||
return { ...prev };
|
||||
});
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
console.error('Article update failed:', err);
|
||||
@@ -100,7 +94,10 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||
setError(null);
|
||||
|
||||
const res = await api.post<ArticleModel>(`/articles`, normalizedArticleData);
|
||||
upsertArticleInList(res.data);
|
||||
setArticles(prev => {
|
||||
prev.create(res.data);
|
||||
return { ...prev };
|
||||
});
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
console.error('Article create failed:', err);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ArticleModel, AuthorModel } from "./models";
|
||||
import {
|
||||
ArticleModel,
|
||||
ArticlesModel,
|
||||
AuthorModel
|
||||
} from "./models";
|
||||
|
||||
export interface ArticleContextModel {
|
||||
articles: ArticleModel[];
|
||||
articles: ArticlesModel;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refreshArticles: () => Promise<void>;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
createInList, readInList, updateInList, deleteInList,
|
||||
createById, readById, updateById, deleteById
|
||||
} from "../utils/articles";
|
||||
|
||||
|
||||
export interface AuthorModel {
|
||||
// meta fields
|
||||
_id?: string | null;
|
||||
@@ -28,3 +34,62 @@ export interface ArticleModel {
|
||||
// ref fields
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ArticleModel } from "./models";
|
||||
import {
|
||||
ArticleModel,
|
||||
ArticlesModel,
|
||||
} from "./models";
|
||||
|
||||
export interface LatestProps {
|
||||
articles: ArticleModel[];
|
||||
articles: ArticlesModel;
|
||||
onSelectArticle?: (index: number) => void;
|
||||
onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback
|
||||
}
|
||||
@@ -12,7 +15,7 @@ export interface LoginProps {
|
||||
}
|
||||
|
||||
export interface MainContentProps {
|
||||
articles: ArticleModel[];
|
||||
articles: ArticlesModel;
|
||||
onSelectArticle: (index: number) => void;
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ export interface ArticleCardProps {
|
||||
}
|
||||
|
||||
export interface ArticleGridProps {
|
||||
articles: ArticleModel[];
|
||||
articles: ArticlesModel;
|
||||
onSelectArticle: (index: number) => void;
|
||||
xs?: number; // default 12 for mobile full-width
|
||||
md12?: number, // default 12 (full-width)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
ArticleModel,
|
||||
ArticleRepoModel
|
||||
} from "../types/models";
|
||||
|
||||
// List helpers
|
||||
|
||||
Reference in New Issue
Block a user