generated from lucien/actix-react-template
back_end_docker #9
22 changed files with 486 additions and 12 deletions
|
@ -74,7 +74,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
.service(api)
|
.service(api)
|
||||||
.service(Files::new("/", "public").index_file("index.html"))
|
.service(Files::new("/", "public").index_file("index.html"))
|
||||||
})
|
})
|
||||||
.bind(("0.0.0.0", 8080))?
|
.bind(("0.0.0.0", 2486))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ services:
|
||||||
container_name: web
|
container_name: web
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:2486
|
||||||
volumes:
|
volumes:
|
||||||
- ./back/data:/app/data
|
- ./back/data:/app/data
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@ RUN cargo build --release
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update & apt-get install -y extra-runtime-dependencies & rm -rf /var/lib/apt/lists/*
|
RUN apt update && apt install -y libsqlite3-0
|
||||||
COPY --from=front /app/dist /app/public
|
COPY --from=front /app/dist /app/public
|
||||||
COPY --from=back /app/target/release/back /app/back
|
COPY --from=back /app/target/release/back /app/back
|
||||||
EXPOSE 8080
|
EXPOSE 2486
|
||||||
CMD ["/app/back"]
|
CMD ["/app/back"]
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router": "^7.0.2"
|
"react-router": "^7.0.2",
|
||||||
|
"react-router-dom": "^6.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.15.0",
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"typescript-eslint": "^8.15.0",
|
"typescript-eslint": "^8.15.0",
|
||||||
"vite": "^6.0.1"
|
"vite": "^6.0.1",
|
||||||
|
"react-router-dom": "^6.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
front/public/pictures/sea.gif
Normal file
BIN
front/public/pictures/sea.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 868 KiB |
49
front/src/components/ArticleCard.tsx
Normal file
49
front/src/components/ArticleCard.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { ArticlePreview } from '../types'
|
||||||
|
import ButtonLink from './ButtonLink'
|
||||||
|
|
||||||
|
export default function ArticleCard({ articlePreview }: { articlePreview: ArticlePreview }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: 'var(--color-yellow)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: 'var(--color-black)',
|
||||||
|
fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '20px',
|
||||||
|
margin: '20px',
|
||||||
|
padding: '1.25em',
|
||||||
|
paddingBottom: '1em',
|
||||||
|
flex: '27%',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}>
|
||||||
|
<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',
|
||||||
|
flexShrink: 0,
|
||||||
|
}} />
|
||||||
|
<div style={{ }}>
|
||||||
|
<h2 style={{
|
||||||
|
fontSize: '25px',
|
||||||
|
fontWeight: '500',
|
||||||
|
lineHeight: '30px',
|
||||||
|
margin: '0',
|
||||||
|
padding: '0',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}>{articlePreview.title}</h2>
|
||||||
|
<p style={{
|
||||||
|
margin: '0',
|
||||||
|
padding: '0',
|
||||||
|
// limit the preview to 200 characters
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
lineClamp: 3,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
}}>{articlePreview.preview}</p>
|
||||||
|
<div style={{ float: 'right', marginTop: '.5rem' }}><ButtonLink url={`/article/${articlePreview.id}`} color='secondary' text='Read more...'/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
48
front/src/components/ArticlesSection.tsx
Normal file
48
front/src/components/ArticlesSection.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ArticleCard from "./ArticleCard";
|
||||||
|
import { ArticlePreview } from "../types";
|
||||||
|
|
||||||
|
export default function ArticlesSection() {
|
||||||
|
|
||||||
|
const [articlePreviews, setArticlePreviews] = useState<ArticlePreview[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/articles')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((data: ArticlePreview[]) => setArticlePreviews(data))
|
||||||
|
.catch(_ => setError('Failed to fetch articles'))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row" style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
backgroundColor: 'var(--color-verydarkblue)',
|
||||||
|
flex: 1,
|
||||||
|
}}>
|
||||||
|
<h1 style={{
|
||||||
|
color: 'var(--color-yellow)',
|
||||||
|
fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '32px',
|
||||||
|
fontWeight: '500',
|
||||||
|
lineHeight: '30px',
|
||||||
|
margin: '20px',
|
||||||
|
padding: '10px',
|
||||||
|
paddingBottom: '0',
|
||||||
|
marginBottom: '10px',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}>Articles</h1>
|
||||||
|
{articlePreviews.map(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>
|
||||||
|
)
|
||||||
|
}
|
35
front/src/components/Button.tsx
Normal file
35
front/src/components/Button.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { ReactNode, MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
color: 'primary' | 'secondary';
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Button({ onClick, color, children }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button 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: '20px',
|
||||||
|
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',
|
||||||
|
}} onClick={onClick} className="ni-button" role="button">{children}</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
34
front/src/components/ButtonLink.tsx
Normal file
34
front/src/components/ButtonLink.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
31
front/src/components/ClickableLink.tsx
Normal file
31
front/src/components/ClickableLink.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
interface ClickableLinkProps {
|
||||||
|
url:string;
|
||||||
|
text:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClickableLink({url, text}: ClickableLinkProps) {
|
||||||
|
return (<div>
|
||||||
|
<a href={url}><button style={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderWidth: '0',
|
||||||
|
color:"purple",
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'inline-block',
|
||||||
|
fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '0',
|
||||||
|
lineHeight: '20px',
|
||||||
|
listStyle: 'none',
|
||||||
|
margin: '0',
|
||||||
|
padding: '10px 12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
transition: 'all 200ms',
|
||||||
|
verticalAlign: 'baseline',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
touchAction: 'manipulation',
|
||||||
|
}} className="LinkNavbar">{text}</button></a>
|
||||||
|
</div>)
|
||||||
|
}
|
19
front/src/components/Footer.tsx
Normal file
19
front/src/components/Footer.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import LogoButton from "./LogoButton"
|
||||||
|
|
||||||
|
interface FooterProps {
|
||||||
|
bgcolor:string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Footer ({bgcolor}:FooterProps) {
|
||||||
|
return (
|
||||||
|
<footer style={{display: "flex", justifyContent: "space-between", backgroundColor:bgcolor}}>
|
||||||
|
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>ENSIBS<br />RedCRAB</div>
|
||||||
|
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>
|
||||||
|
<LogoButton
|
||||||
|
url="https://www-ensibs.univ-ubs.fr/fr/index.html"
|
||||||
|
logo = "https://www-ensibs.univ-ubs.fr/skins/ENSIBS/resources/img/logo.png"
|
||||||
|
style={{ width: "50%", height: "auto" }}/>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
38
front/src/components/FstSection.tsx
Normal file
38
front/src/components/FstSection.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import Button from "./Button";
|
||||||
|
import NavBar from "./NavBar";
|
||||||
|
|
||||||
|
interface FstSectionProps {
|
||||||
|
centertxt: string;
|
||||||
|
txtbt1:string;
|
||||||
|
txtbt2:string;
|
||||||
|
image:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FstSection ({centertxt, txtbt1, txtbt2, image}: FstSectionProps) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
textAlign: "center",
|
||||||
|
color: "yellow",
|
||||||
|
backgroundImage: image,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
height: "75vh"}}>
|
||||||
|
<NavBar />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{height:"150px"}} />
|
||||||
|
<h1>{centertxt}</h1><br />
|
||||||
|
<div style={{display:"flex", justifyContent: "center", gap: "20px"}}>
|
||||||
|
<Button onClick={() => alert("One")} color="primary" children={txtbt1} />
|
||||||
|
<Button onClick={() => {alert("Two")}} color="secondary" children={txtbt2} />
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
)
|
||||||
|
}
|
13
front/src/components/LogoButton.tsx
Normal file
13
front/src/components/LogoButton.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface LogoButtonProps {
|
||||||
|
url:string;
|
||||||
|
logo:string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogoButton ({url, logo, style}: LogoButtonProps) {
|
||||||
|
return (
|
||||||
|
<a href={url}><img src={logo} style={{ ...style }}/></a>
|
||||||
|
)
|
||||||
|
}
|
17
front/src/components/NavBar.tsx
Normal file
17
front/src/components/NavBar.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import LogoButton from '../components/LogoButton.tsx'
|
||||||
|
import ClickableLink from './ClickableLink.tsx';
|
||||||
|
import RoundButton from './RoundButton.tsx';
|
||||||
|
|
||||||
|
export default function NavBar(){
|
||||||
|
return (
|
||||||
|
<nav style={{ display: "flex", alignItems: "center", justifyContent: "space-between"}}>
|
||||||
|
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>
|
||||||
|
<LogoButton url="/" logo = "https://archlinux.org/static/hetzner_logo.41114a37d25f.png"/>
|
||||||
|
<ClickableLink url="/" text = "Accueil" />
|
||||||
|
<ClickableLink url="/game" text = "Jeu" />
|
||||||
|
</div>
|
||||||
|
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>
|
||||||
|
<RoundButton url="https://archlinux.org" bgcolor="purple" text="?"/>
|
||||||
|
</div>
|
||||||
|
</nav>);
|
||||||
|
}
|
32
front/src/components/RoundButton.tsx
Normal file
32
front/src/components/RoundButton.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
interface RoundButtonProps {
|
||||||
|
url:string;
|
||||||
|
bgcolor:string;
|
||||||
|
text:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RoundButton({url, bgcolor, text}: RoundButtonProps) {
|
||||||
|
return (<div>
|
||||||
|
<a href={url}><button style={{
|
||||||
|
backgroundColor: bgcolor,
|
||||||
|
borderColor:"blue",
|
||||||
|
borderRadius: '50%',
|
||||||
|
borderWidth: '1',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'inline-block',
|
||||||
|
fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '750',
|
||||||
|
lineHeight: '20px',
|
||||||
|
listStyle: 'none',
|
||||||
|
margin: '0',
|
||||||
|
padding: '10px 12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
transition: 'all 200ms',
|
||||||
|
verticalAlign: 'baseline',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
touchAction: 'manipulation',
|
||||||
|
}} className="RoundButtonNavBar">{text}</button></a>
|
||||||
|
</div>)
|
||||||
|
}
|
19
front/src/index.css
Normal file
19
front/src/index.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/* Define theme colors */
|
||||||
|
:root {
|
||||||
|
--color-verydarkblue: #00204a;
|
||||||
|
--color-darkblue: #005792;
|
||||||
|
--color-lightblue: #00bbf0;
|
||||||
|
--color-yellow: #fdb44b;
|
||||||
|
--color-white: #ffffff;
|
||||||
|
--color-black: #000000;
|
||||||
|
--color-gray: #f5f5f5;
|
||||||
|
--color-red: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
||||||
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>
|
||||||
)
|
)
|
||||||
|
|
78
front/src/pages/ArticlePage.tsx
Normal file
78
front/src/pages/ArticlePage.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import NavBar from '../components/NavBar';
|
||||||
|
import Footer from '../components/Footer';
|
||||||
|
|
||||||
|
interface Article {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
author: string;
|
||||||
|
publishedAt: string;
|
||||||
|
editedAt: 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 style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100vh'
|
||||||
|
}}>
|
||||||
|
<NavBar/>
|
||||||
|
<div style={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: 'var(--color-black)',
|
||||||
|
fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '20px',
|
||||||
|
padding: '1.25em',
|
||||||
|
paddingBottom: '1em',
|
||||||
|
textAlign: 'left',
|
||||||
|
maxWidth: '60rem',
|
||||||
|
margin: 'auto',
|
||||||
|
}}>
|
||||||
|
<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',
|
||||||
|
}} />
|
||||||
|
<div>
|
||||||
|
<h1>{article.title}</h1>
|
||||||
|
<p>écrit par {article.author} le {new Date(article.publishedAt).toLocaleDateString()} (Dernière modification le : {new Date(article.editedAt || article.publishedAt).toLocaleDateString()})</p>
|
||||||
|
<hr/>
|
||||||
|
<p>{article.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer bgcolor='lightblue'/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArticlePage;
|
|
@ -1,10 +1,19 @@
|
||||||
|
import ArticlesSection from '../components/ArticlesSection.tsx'
|
||||||
|
import Footer from '../components/Footer.tsx'
|
||||||
|
import FstSection from '../components/FstSection.tsx'
|
||||||
|
|
||||||
export default function MainPage() {
|
export default function MainPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h1>Main Page</h1>
|
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||||
|
<FstSection
|
||||||
|
centertxt="Choisissez votre numéro :"
|
||||||
|
txtbt1="Un"
|
||||||
|
txtbt2="Deux"
|
||||||
|
image="url('/pictures/sea.gif')"/>
|
||||||
|
<ArticlesSection />
|
||||||
|
<Footer bgcolor="lightblue"/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
13
front/src/types.ts
Normal file
13
front/src/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
interface Article {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArticlePreview {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
preview: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Article, ArticlePreview };
|
|
@ -4,4 +4,28 @@ import react from '@vitejs/plugin-react'
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:2486',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
/*
|
||||||
|
configure: (proxy, _options) => {
|
||||||
|
proxy.on('error', (err, _req, _res) => {
|
||||||
|
console.log('proxy error', err);
|
||||||
|
});
|
||||||
|
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||||
|
console.log('Sending Request to the Target:', req.method, req.url);
|
||||||
|
});
|
||||||
|
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
||||||
|
console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
//rewrite: path => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "projet-nuitinfo-2024",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue