7 Commits

8 changed files with 110 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "aetoskia-blog-app",
"version": "0.2.2",
"version": "0.2.3",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -179,6 +179,7 @@ export default function ArticleView({
title,
tag,
img,
description,
content,
})
}

View File

@@ -55,7 +55,14 @@ export default function ArticleView({
<Divider sx={{ my: 3 }} />
<CoverImage src={article.img} alt={article.title} />
<CoverImage
src={(
import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") +
"/" +
(article.img?.replace(/^\/+/, "") || "")
)}
alt={article.title}
/>
<Box
sx={{

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { CardMedia, Typography } from '@mui/material';
import { ArticleMeta } from "../ArticleMeta";
import { ArticleCardProps } from "../../types/props";
import { StyledCard, StyledCardContent, StyledTypography } from "../../types/styles";
export default function ArticleCardSize12({
article,
index,
focusedCardIndex,
onSelectArticle,
onFocus,
onBlur,
}: ArticleCardProps) {
return (
<StyledCard
variant="outlined"
onClick={() => onSelectArticle(index)}
onFocus={() => onFocus(index)}
onBlur={onBlur}
tabIndex={0}
className={focusedCardIndex === index ? 'Mui-focused' : ''}
>
<CardMedia
component="img"
alt={article.title}
image={(
import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") +
"/" +
(article.img?.replace(/^\/+/, "") || "")
)}
sx={{
aspectRatio: '16 / 9',
}}
/>
<StyledCardContent>
<Typography gutterBottom variant="caption" component="div">
{article.tag}
</Typography>
<Typography gutterBottom variant="h6" component="div">
{article.title}
</Typography>
<StyledTypography variant="body2" color="text.secondary" gutterBottom>
{article.description}
</StyledTypography>
</StyledCardContent>
<ArticleMeta article={article} />
</StyledCard>
);
};

View File

@@ -24,7 +24,7 @@ export default function ArticleCardSize4({
>
<CardMedia
component="img"
alt="green iguana"
alt={article.title}
image={(
import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") +
"/" +

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Grid, Box } from '@mui/material';
import ArticleCardSize12 from './ArticleCardSize12';
import ArticleCardSize6 from './ArticleCardSize6';
import ArticleCardSize4 from './ArticleCardSize4';
import ArticleCardSize2 from './ArticleCardSize2';
@@ -10,6 +11,7 @@ export default function ArticleCardsGrid({
articles,
onSelectArticle,
xs = 12,
md12 = 12,
md6 = 6,
md4 = 4,
nested = 2,
@@ -30,8 +32,9 @@ export default function ArticleCardsGrid({
setFocusedCardIndex(null);
};
const renderCard = (article: ArticleModel, index: number, type: '6' | '4' | '2' = '6') => {
const renderCard = (article: ArticleModel, index: number, type: '12' | '6' | '4' | '2' = '12') => {
const CardComponent =
type === '12' ? ArticleCardSize12 :
type === '6' ? ArticleCardSize6 :
type === '4' ? ArticleCardSize4 :
ArticleCardSize2;
@@ -51,6 +54,17 @@ export default function ArticleCardsGrid({
return (
<Grid container spacing={2} columns={12}>
{/* ---- 1 article: 12 ---- */}
{count === 1 && (
<>
{visibleArticles.map((a, i) => (
<Grid key={i} size={{ xs, md: md12 }}>
{renderCard(a, i, '12')}
</Grid>
))}
</>
)}
{/* ---- 2 articles: 6 | 6 ---- */}
{count === 2 && (
<>

View File

@@ -10,15 +10,38 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
const [articles, setArticles] = useState<ArticleModel[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const { token } = 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 */
const normalizeArticleForApi = (article: Partial<ArticleModel>) => ({
...article,
authors: (article.authors ?? []).map(a =>
a._id
),
});
const normalizeArticleForApi = (article: Partial<ArticleModel>) => {
// Extract existing authors as a list of IDs (string[])
const existingIds = (article.authors ?? []).map(a =>
typeof a === "string" ? a : a._id
);
// Inject currentUser if missing
const allAuthorIds = currentUser?._id
? Array.from(new Set([...existingIds, currentUser._id])) // dedupe
: existingIds;
return {
...article,
authors: allAuthorIds,
};
};
/** 🔹 Fetch articles (JWT automatically attached by api.ts interceptor) */
const fetchArticles = async () => {
@@ -50,6 +73,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
setError(null);
const res = await api.put<ArticleModel>(`/articles/${articleData._id}`, normalizedArticleData);
upsertArticleInList(res.data);
return res.data;
} catch (err: any) {
console.error('Article update failed:', err);
@@ -72,6 +96,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
setError(null);
const res = await api.post<ArticleModel>(`/articles`, normalizedArticleData);
upsertArticleInList(res.data);
return res.data;
} catch (err: any) {
console.error('Article create failed:', err);

View File

@@ -34,6 +34,7 @@ export interface ArticleGridProps {
articles: ArticleModel[];
onSelectArticle: (index: number) => void;
xs?: number; // default 12 for mobile full-width
md12?: number, // default 12 (full-width)
md6?: number; // default 6 (half-width)
md4?: number; // default 4 (third-width)
nested?: 1 | 2; // number of stacked cards in a nested column