back_end_docker #9

Merged
lucien merged 23 commits from back_end_docker into back_end 2024-12-06 03:36:58 +00:00
7 changed files with 111 additions and 26 deletions
Showing only changes of commit e121090433 - Show all commits

View file

@ -1,5 +1,5 @@
import { ArticlePreview } from '../types' import { ArticlePreview } from '../types'
import Button from './Button' import ButtonLink from './ButtonLink'
export default function ArticleCard({ articlePreview }: { articlePreview: ArticlePreview }) { export default function ArticleCard({ articlePreview }: { articlePreview: ArticlePreview }) {
return ( return (
@ -42,7 +42,7 @@ export default function ArticleCard({ articlePreview }: { articlePreview: Articl
lineClamp: 3, lineClamp: 3,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
}}>{articlePreview.preview}</p> }}>{articlePreview.preview}</p>
<div style={{ float: 'right', marginTop: '.5rem' }}><Button color='secondary' onClick={() => alert('Not implemented')}>Read more...</Button></div> <div style={{ float: 'right', marginTop: '.5rem' }}><ButtonLink url={`/article/${articlePreview.id}`} color='secondary' text='Read more...'/></div>
</div> </div>
</div> </div>
) )

View file

@ -5,30 +5,15 @@ import { ArticlePreview } from "../types";
export default function ArticlesSection() { export default function ArticlesSection() {
const [articlePreviews, setArticlePreviews] = useState<ArticlePreview[]>([]); const [articlePreviews, setArticlePreviews] = useState<ArticlePreview[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetch('/api/article') fetch('/api/article')
.then(response => { .then(response => response.json())
//response.json() .then((data: ArticlePreview[]) => setArticlePreviews(data))
return [ .catch(_ => setError('Failed to fetch articles'))
{ .finally(() => setLoading(false));
id: 1,
title: "Qu'est-ce que le Lorem Ipsum?",
preview: "Le Lorem Ipsum est simplement du faux texte employé dans la composition et la mise en page avant impression. Le Lorem Ipsum est le faux texte standard de l'imprimerie depuis les années 1500, quand un imprimeur anonyme assembla ensemble des morceaux de texte pour réaliser un livre spécimen de polices de texte. Il n'a pas fait que survivre cinq siècles, mais s'est aussi adapté à la bureautique informatique, sans que son contenu n'en soit modifié. Il a été popularisé dans les années 1960 grâce à la vente de feuilles Letraset contenant des passages du Lorem Ipsum, et, plus récemment, par son inclusion dans des applications de mise en page de texte, comme Aldus PageMaker.",
},
{
id: 2,
title: "Pourquoi l'utiliser?",
preview: "On sait depuis longtemps que travailler avec du texte lisible et contenant du sens est source de distractions, et empêche de se concentrer sur la mise en page elle-même. L'avantage du Lorem Ipsum sur un texte générique comme 'Du texte. Du texte. Du texte.' est qu'il possède une distribution de lettres plus ou moins normale, et en tout cas comparable avec celle du français standard. De nombreuses suites logicielles de mise en page ou éditeurs de sites Web ont fait du Lorem Ipsum leur faux texte par défaut, et une recherche pour 'Lorem Ipsum' vous conduira vers de nombreux sites qui n'en sont encore qu'à leur phase de construction. Plusieurs versions sont apparues avec le temps, parfois par accident, souvent intentionnellement (histoire d'y rajouter de petits clins d'oeil, voire des phrases embarassantes).",
},
{
id: 3,
title: "Title",
preview: "Preview",
},
]
})
.then((data: ArticlePreview[]) => setArticlePreviews(data));
}, []); }, []);
return ( return (
@ -37,6 +22,7 @@ export default function ArticlesSection() {
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-around', justifyContent: 'space-around',
backgroundColor: 'var(--color-verydarkblue)', backgroundColor: 'var(--color-verydarkblue)',
flex: 1,
}}> }}>
<h1 style={{ <h1 style={{
color: 'var(--color-yellow)', color: 'var(--color-yellow)',
@ -54,6 +40,9 @@ export default function ArticlesSection() {
{articlePreviews.map(articlePreview => ( {articlePreviews.map(articlePreview => (
<ArticleCard key={articlePreview.id} articlePreview={articlePreview} /> <ArticleCard key={articlePreview.id} articlePreview={articlePreview} />
))} ))}
{!loading && !error && articlePreviews.length === 0 && <p style={{color: "var(--color-lightblue)", fontSize: 22}}>No articles found</p>}
{loading && <p style={{color: "var(--color-lightblue)", fontSize: 22}}>Loading...</p>}
{error && <p style={{color: "var(--color-lightblue)", fontSize: 22}}>{error}</p>}
</div> </div>
) )
} }

View file

@ -0,0 +1,34 @@
interface ButtonLinkProps {
url: string;
color: 'primary' | 'secondary';
text: string;
}
export default function Button({ url, color, text }: ButtonLinkProps) {
return (
<a style={{
backgroundColor: color === 'primary' ? 'var(--color-yellow)' : 'var(--color-darkblue)',
borderRadius: '8px',
borderWidth: '0',
color: color === 'primary' ? 'var(--color-black)' : 'var(--color-white)',
cursor: 'pointer',
display: 'inline-block',
fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif',
fontSize: '14px',
fontWeight: '500',
lineHeight: '20px',
listStyle: 'none',
margin: '0',
padding: '10px 12px',
textAlign: 'center',
transition: 'all 200ms',
verticalAlign: 'baseline',
whiteSpace: 'nowrap',
userSelect: 'none',
WebkitUserSelect: 'none',
touchAction: 'manipulation',
textDecoration: 'none',
}} href={url} className="ni-button">{text}</a>
)
}

View file

@ -7,6 +7,7 @@
--color-white: #ffffff; --color-white: #ffffff;
--color-black: #000000; --color-black: #000000;
--color-gray: #f5f5f5; --color-gray: #f5f5f5;
--color-red: #ff0000;
} }
body { body {

View file

@ -2,13 +2,20 @@ import { BrowserRouter, Route, Routes } from "react-router";
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import MainPage from "./pages/MainPage.tsx"; import MainPage from "./pages/MainPage.tsx";
import GamePage from "./pages/GamePage.tsx"; import GamePage from "./pages/GamePage.tsx";
import ArticlePage from "./pages/ArticlePage.tsx";
import './index.css' import './index.css'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
// Main page
<Route path="/" element={<MainPage />} /> <Route path="/" element={<MainPage />} />
// Game page
<Route path="/game" element={<GamePage />} /> <Route path="/game" element={<GamePage />} />
// Article page (dynamic route)
<Route path="/article/:id" element={<ArticlePage />} />
// Not found
<Route path="*" element={<div>Not Found</div>} />
</Routes> </Routes>
</BrowserRouter>, </BrowserRouter>
) )

View file

@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
interface Article {
id: string;
title: string;
content: string;
author: string;
publishedAt: string;
}
const ArticlePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/article/${id}`)
.then(response => response.json())
.then(data => setArticle(data))
.catch(_ => setError('Failed to fetch article'))
.finally(() => setLoading(false));
}, [id]);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>{error}</div>;
}
if (!article) {
return <div>Article not found</div>;
}
return (
<div>
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fimages8.alphacoders.com%2F438%2F438396.jpg&f=1&nofb=1&ipt=14796576c139b58db62d2d2098392ef2fbaee77911040f7c0d98ab65a69c4644&ipo=images" alt="Article preview" style={{
borderRadius: '8px',
width: '100%',
height: 'auto',
marginBottom: '10px',
}} />
<h1>{article.title}</h1>
<p>{article.content}</p>
<p>By {article.author}</p>
<p>Published on {new Date(article.publishedAt).toLocaleDateString()}</p>
</div>
);
};
export default ArticlePage;

View file

@ -5,11 +5,11 @@ import Footer from '../components/Footer.tsx'
export default function MainPage() { export default function MainPage() {
return ( return (
<> <>
<div> <div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<NavBar /> <NavBar />
<ArticlesSection /> <ArticlesSection />
<Footer bgcolor="blue"/>
</div> </div>
<Footer bgcolor="blue"/>
</> </>
) )
} }