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 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);
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ArticleModel,
|
ArticleModel,
|
||||||
ArticleRepoModel
|
|
||||||
} from "../types/models";
|
} from "../types/models";
|
||||||
|
|
||||||
// List helpers
|
// List helpers
|
||||||
|
|||||||
Reference in New Issue
Block a user