Compare commits

...

59 commits

Author SHA1 Message Date
SwimJéjé
53853dcce5 Merge branch 'main' of ssh://git.leizour.fr:222/RedCrab/projet-nuitinfo-2024 into chaos_page 2024-12-06 05:40:57 +01:00
iMax
cbae2746fd Merge branch 'main_page' into main 2024-12-06 05:07:10 +01:00
iMax
12fa0684a8 Changement de nom d'application. 2024-12-06 05:04:36 +01:00
iMax
3794ccd70d Merge branch 'main_page' of ssh://git.leizour.fr:222/RedCrab/projet-nuitinfo-2024 into main_page 2024-12-06 04:53:32 +01:00
b252122272 Merged nia 2024-12-06 04:44:34 +01:00
iMax
5bb2ac3f9b - Mise à jour de la DA de la main page.
- Ajout de data temporaire pour les articles.
2024-12-06 04:43:17 +01:00
467d560b15 Merge pull request 'back_end_docker' (#9) from back_end_docker into back_end
Reviewed-on: #9
2024-12-06 03:36:57 +00:00
ac3fc5c391 Updated ports 2024-12-06 04:34:40 +01:00
=
71a829e726 Edit hero banner 2024-12-06 04:06:07 +01:00
d6f2d6cc09 VALIDER PAR OLIVIER : Merge pull request 'better api and yyyy/mm/dd format for dates in the db' (#8) from back_end_id into main
Reviewed-on: #8
2024-12-06 02:58:19 +00:00
e42b81539f Merge branch 'main' into back_end_id 2024-12-06 02:57:42 +00:00
ec2322f04c better api and yyyy/mm/dd format for dates in the db 2024-12-06 03:52:12 +01:00
iMax
1de47f3fb8 Hot fix 2024-12-06 03:51:26 +01:00
iMax
17affb3297 Merge branch 'main' of ssh://git.leizour.fr:222/RedCrab/projet-nuitinfo-2024 into main_page 2024-12-06 03:50:44 +01:00
1e956a54d2 Merge pull request 'Game feature complete' (#7) from game into main
Reviewed-on: #7
2024-12-06 02:41:27 +00:00
9ee9ae8cb7 Merge branch 'main' into game 2024-12-06 02:40:26 +00:00
iMax
f8c4481c88 - Feature Complete -> Gray Boxing a faire. 2024-12-06 03:37:26 +01:00
=
bf73ed1698 api proxy working :) 2024-12-06 03:25:50 +01:00
ChenNux
e53fc84ac3 modifié : src/components/NavBar.tsx 2024-12-06 03:24:53 +01:00
=
5b812000b8 Merge branch 'main' into main_page 2024-12-06 02:51:57 +01:00
=
e2261f829c Add api proxy 2024-12-06 02:50:16 +01:00
ee692fec27 Actualiser back/src/main.rs 2024-12-06 01:50:00 +00:00
ChenNux
2adacceac5 modifié : src/pages/MainPage.tsx
+ une petite correction
2024-12-06 02:07:00 +01:00
ChenNux
de11f64cc3 nouveau fichier : public/pictures/sea.gif
modifié :         src/components/Button.tsx
	modifié :         src/components/ClickableLink.tsx
	nouveau fichier : src/components/FstSection.tsx
	modifié :         src/components/NavBar.tsx
	modifié :         src/pages/MainPage.tsx

ajout de la première section
2024-12-06 02:06:15 +01:00
ac20ecc932 Merge pull request 'back_end' (#6) from back_end into main
Reviewed-on: #6
2024-12-06 00:54:17 +00:00
iMax
60a12d9cf9 - Ajout d'un océan.
- Ajout d'un personnage manequin.
--> *Modèle 3D du manequin inclus*
- Ajout d'une représentation des axes x,y,z.
- Ajout d'une entité Marker pour mettre des markers sur le manequin.
- Maj du CSS de GamePage.tsx
2024-12-06 01:51:30 +01:00
e2575b55b6 Actualiser back/Cargo.toml 2024-12-06 00:43:11 +00:00
3688d031b4 Actualiser back/src/main.rs
finito ?
2024-12-06 00:42:10 +00:00
=
4e7259ae28 add react-router-dom to package.json 2024-12-06 01:34:45 +01:00
9b91a5acf3 Updated docker-compose.yml 2024-12-06 01:33:19 +01:00
85ab4aba4f Fixed databse init function again 2024-12-06 01:31:27 +01:00
8f8fc0c46e Fixed databse init function 2024-12-06 01:30:34 +01:00
=
e121090433 Make a better article section and add the article page. 2024-12-06 01:28:36 +01:00
902e96b205 Added database init function 2024-12-06 01:00:02 +01:00
=
9562eb98dc Articles section of the main page 2024-12-06 00:37:52 +01:00
ChenNux
f77b96db25 nouveau fichier : front/src/components/Footer.tsx 2024-12-06 00:36:52 +01:00
241a783092 Merge pull request 'Merged back_end to main branch' (#5) from back_end into main
Reviewed-on: #5
2024-12-05 23:29:13 +00:00
ChenNux
6e67e1238b modifié : front/src/pages/MainPage.tsx
front/src/components/Footer.tsx
2024-12-06 00:29:09 +01:00
6461ebc629 Merged back_end and back_end_json 2024-12-06 00:26:12 +01:00
ChenNux
7d8dd263eb modifié : front/src/components/LogoButton.tsx
modifié :         front/src/components/NavBar.tsx
	modifié :         front/src/index.css
	modifié :         front/src/pages/MainPage.tsx
	front/src/components/Footer.tsx

Ajout du footer
2024-12-06 00:23:24 +01:00
8316a35e9e Added json usage 2024-12-06 00:22:01 +01:00
c3a7d0ee7b Merge pull request 'Actualiser back/src/main.rs' (#4) from back_end into main
Reviewed-on: #4
2024-12-05 23:03:41 +00:00
c3888c61f9 Actualiser back/src/main.rs
added the service bc i forgot oupsie

Signed-off-by: linlkin <ilyass.ajdira@hotmail.com>
2024-12-05 23:02:43 +00:00
ChenNux
a239d17697 modifié : front/src/components/ClickableLink.tsx
modifié :         front/src/components/LogoButton.tsx
	modifié :         front/src/components/RoundButton.tsx

Insertion des types pour les composants.
2024-12-05 23:42:38 +01:00
9fca283ab1 Merge pull request 'i added db and articles end points' (#1) from back_end into main
Reviewed-on: #1
2024-12-05 22:42:28 +00:00
efe0b6b410 Merge branch 'main' into back_end 2024-12-05 22:42:15 +00:00
669c710f89 Added json usage 2024-12-05 23:38:43 +01:00
ChenNux
b3d966e0ae nouveau fichier : front/src/components/RoundButton.tsx 2024-12-05 23:36:21 +01:00
ChenNux
2317a3683e modifié : front/src/components/ClickableLink.tsx
modifié :         front/src/components/LogoButton.tsx
	modifié :         front/src/components/NavBar.tsx
	front/src/components/RoundButton.tsx
2024-12-05 23:36:21 +01:00
57d574d143 i added db and articles end points 2024-12-05 23:19:15 +01:00
=
36dae30b4e Add first version of article section 2024-12-05 22:59:11 +01:00
=
5c4ac78dee add style inline for the button component and theme colors 2024-12-05 22:25:56 +01:00
iMax
5d156416c9 Recollage au main 2024-12-05 22:15:23 +01:00
iMax
358681da38 Merge branch 'main' into game 2024-12-05 22:12:59 +01:00
iMax
f3dc0e99f8 Merge branch 'main' of ssh://git.leizour.fr:222/RedCrab/projet-nuitinfo-2024 into HEAD 2024-12-05 21:57:00 +01:00
ChenNux
3cdab2e303 modifié : front/src/components/ClickableLink.tsx
modifié :         front/src/components/NavBar.tsx

C'est très moche (j'aimerais avoir le bouton lien pas une ligne en dessous le logo lien);
2024-12-05 21:47:03 +01:00
iMax
706d491236 - implémentation de threejs.
- Création environnement 3D océan.
2024-12-05 21:42:37 +01:00
ChenNux
265157bbe1 nouveau fichier : front/src/components/ClickableLink.tsx
nouveau fichier : front/src/components/LogoButton.tsx
	nouveau fichier : front/src/components/NavBar.tsx
	modifié :         front/src/main.tsx
	modifié :         front/src/pages/MainPage.tsx
2024-12-05 21:34:24 +01:00
=
ebf4620640 New button component 2024-12-05 21:19:41 +01:00
34 changed files with 1062 additions and 20 deletions

3
.gitignore vendored
View file

@ -1,5 +1,6 @@
back/target/ back/target/
front/dist/ front/dist/
back/public/ back/public/
back/data/
back/Cargo.lock
back/Cargo.lock

View file

@ -6,4 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
actix-files = "0.6.6" actix-files = "0.6.6"
actix-web = "4" actix-web = "4"
sqlite = "0.36.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.133"

23
back/src/create_db.rs Normal file
View file

@ -0,0 +1,23 @@
use sqlite::{Connection, OpenFlags};
pub fn init() -> sqlite::Result<()> {
let conn = Connection::open_with_flags(
"./data/data.db",
OpenFlags::new()
.with_create()
.with_read_write()
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
auteur TEXT,
edited_at DATE_FORMAT('now', '%YYYY-%mm-%dd'),
published_at DATE_FORMAT('now', '%YYYY-%mm-%dd'),
content TEXT NOT NULL
)",
)?;
Ok(())
}

View file

@ -1,13 +1,129 @@
use actix_web::{App, HttpServer}; mod create_db;
use create_db::init;
use actix_web::{App, web, HttpServer, get, Responder, HttpResponse, http::header::ContentType};
use actix_files::Files; use actix_files::Files;
use serde_json::json;
use sqlite::{Connection, State};
use serde::{Serialize, Deserialize};
#[derive(serde::Serialize)]
struct Article {
id: i64,
title: String,
auteur: String,
edited_at: String,
published_at: String,
content: String,
}
#[get("/api/hello")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[get("/api/articles")]
async fn get_articles() -> impl Responder {
let conn = Connection::open("./data/data.db").unwrap();
let mut stmt = conn.prepare("SELECT * FROM articles").unwrap();
let mut articles = Vec::new();
while let State::Row = stmt.next().unwrap() {
let id = stmt.read::<i64, _>(0).unwrap();
let title = stmt.read::<String, _>(1).unwrap();
let content = stmt.read::<String, _>(5).unwrap();
articles.push(Article {
id,
title,
auteur: "".to_string(),
edited_at: "".to_string(),
published_at: "".to_string(),
content
});
}
HttpResponse::Ok().json(articles)
}
#[get("/api/articles/{id}")]
async fn get_article(path: web::Path<i64>) -> impl Responder {
let id = path.into_inner();
// Open the database connection
let conn = match Connection::open("./data/data.db") {
Ok(conn) => conn,
Err(err) => {
eprintln!("Failed to connect to database: {}", err);
return HttpResponse::InternalServerError().body("Failed to connect to database");
}
};
// Fetch the article from the database
match fetch_article_by_id(&conn, id) {
Ok(Some(article)) => HttpResponse::Ok().json(article),
Ok(None) => HttpResponse::NotFound().body(format!("Article with ID {} not found", id)),
Err(err) => {
eprintln!("Database query error: {}", err);
HttpResponse::InternalServerError().body("Database query failed")
}
}
}
/// Fetches an article by its ID from the database.
fn fetch_article_by_id(conn: &Connection, id: i64) -> Result<Option<Article>, sqlite::Error> {
let mut stmt = conn.prepare(
"SELECT id, title, auteur, edited_at, published_at, content
FROM articles WHERE id = ?1"
)?;
stmt.bind((1, id))?;
let mut article = None;
while let State::Row = stmt.next()? {
article = Some(Article {
id: stmt.read::<i64, _>(0)?,
title: stmt.read::<String, _>(1)?,
auteur: stmt.read::<String, _>(2)?,
edited_at: stmt.read::<String, _>(3)?,
published_at: stmt.read::<String, _>(4)?,
content: stmt.read::<String, _>(5)?,
});
}
Ok(article)
}
#[get("/api")]
async fn api() -> impl Responder {
let value = json!({
"code": 200,
"success": true,
"payload": {
"features": [
"serde",
"json"
],
"homepage": null
}
});
HttpResponse::Ok()
.content_type(ContentType::json())
.body(value.to_string())
}
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> Result<(), std::io::Error> {
let _ = init();
HttpServer::new(|| { HttpServer::new(|| {
App::new() App::new()
.service(Files::new("/", "./public").index_file("index.html")) .service(hello)
.service(get_articles)
.service(api)
.service(get_article)
.service(Files::new("/", "public").index_file("index.html"))
}) })
.bind(("0.0.0.0", 8080))? .bind(("0.0.0.0", 2486))?
.run() .run()
.await .await
} }

View file

@ -6,5 +6,7 @@ services:
container_name: web container_name: web
restart: always restart: always
ports: ports:
- 8080:8080 - 8080:2486
volumes:
- ./back/data:/app/data

View file

@ -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"]

View file

@ -4,7 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>EcoMarin</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=sailing" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -10,9 +10,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@react-three/drei": "^9.120.0",
"@react-three/fiber": "^8.17.10",
"@types/three": "^0.170.0",
"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",
"three": "^0.171.0",
"three-stdlib": "^2.34.0",
"react-router-dom": "^6.2.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.15.0", "@eslint/js": "^9.15.0",
@ -25,6 +31,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/models/man.glb Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

View file

@ -0,0 +1,31 @@
import React from 'react'
import { LineBasicMaterial, Line, Group } from 'three'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
export default function Axes() {
const axes = new Group()
const x = new Line(
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0),
new THREE.Vector3(100, 0, 0)]),
new LineBasicMaterial({ color: 0xff0000 }) // red
)
const y = new Line(
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 100, 0)]),
new LineBasicMaterial({ color: 0x00ff00 }) // green
)
const z = new Line(
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 100)]),
new LineBasicMaterial({ color: 0x0000ff }) // blue
)
axes.add(x)
axes.add(y)
axes.add(z)
return <primitive object={axes} />
}

View file

@ -0,0 +1,25 @@
import { Group, Mesh, MeshStandardMaterial, BufferGeometry } from 'three'
import { useGLTF } from '@react-three/drei'
export default function Character() {
// import glb file
// load the glb file in "/models/BASEmodel.glb"
const { nodes, materials, scene } = useGLTF('/models/man.glb')
// rotate the character
scene.rotation.x = -Math.PI / 2
scene.rotation.y = 0
// enfoncer le personnage dans le sol
scene.position.y = -1
return (
<group>
<primitive object={scene} />
<directionalLight position={[0, 10, 0]} intensity={5} />
</group>
)
}

View file

@ -0,0 +1,20 @@
import React from 'react'
import { Group } from 'three'
import * as THREE from 'three'
export default function Floor() {
const floor = new Group()
const floorGeometry = new THREE.PlaneGeometry(100, 100, 100, 100)
const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00, side: THREE.DoubleSide })
const floorMesh = new THREE.Mesh(floorGeometry, floorMaterial)
floorMesh.rotation.x = -Math.PI / 2
floorMesh.position.y = -2
floor.add(floorMesh)
return <primitive object={floor} />
}

View file

@ -0,0 +1,23 @@
import React from 'react'
import { Group } from 'three'
import * as THREE from 'three'
interface MarkerProps {
position: [number, number, number],
color: string,
onClick?: () => void
}
export default function Marker({ position, color, onClick }: MarkerProps) {
const [positionState, setPositionState] = React.useState(position)
// Return the marker object
// return <primitive object={marker} />
return (
<mesh position={positionState} rotation={[Math.PI,0,0]} onClick={onClick} onPointerOver={(e) => setPositionState([positionState[0], positionState[1], positionState[2] + 0.1])} onPointerOut={(e) => setPositionState(position)}>
<coneGeometry args={[0.15, 0.6, 6]} />
<meshStandardMaterial color={color} side={THREE.DoubleSide} />
</mesh>
)
}

View file

@ -0,0 +1,53 @@
// src/components/Ocean.tsx
import React, { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { Water, WaterOptions } from 'three/examples/jsm/objects/Water.js';
import { WaterMesh, WaterMeshOptions } from 'three/examples/jsm/objects/Water2Mesh.js';
const Ocean: React.FC = () => {
const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
const waterOption:WaterOptions = {
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load('https://threejs.org/examples/textures/waternormals.jpg', function (texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}),
alpha: 0.9,
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: '#001e0f',
distortionScale: 3.7,
fog: true,
};
const water = new Water(waterGeometry, waterOption);
water.rotation.x = -Math.PI / 2;
water.position.y = -1;
const waterRef = useRef<THREE.Mesh>(null);
useEffect(() => {
if (waterRef.current) {
waterRef.current.add(water);
}
}, [waterRef]);
useEffect(() => {
const animate = () => {
requestAnimationFrame(animate);
water.material.uniforms['time'].value += 0.1 / 60.0;
};
animate();
}, []);
return (
<mesh ref={waterRef}>
<meshStandardMaterial />
</mesh>
);
};
export default Ocean;

View 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: '0 1 30%',
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>
)
}

View file

@ -0,0 +1,83 @@
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);
const articles: ArticlePreview[] = [
{
id: 1,
title: "The Great Barrier Reef",
preview: "The Great Barrier Reef is the largest coral reef system in the world, composed of over 2,900 individual reefs and 900 islands stretching for over 2,300 kilometers (1,400 mi) over an area of approximately 344,400 square kilometers (133,000 sq mi).",
},
{
id: 2,
title: "The Great Pacific Garbage Patch",
preview: "The Great Pacific Garbage Patch, also known as the Pacific trash vortex, is a gyre of marine debris particles in the north-central Pacific Ocean. It is located roughly from 135°W to 155°W and 35°N to 42°N.",
},
{
id: 3,
title: "The Amazon Rainforest",
preview: "The Amazon rainforest, alternatively, the Amazon Jungle, also known in English as Amazonia, is a moist broadleaf tropical rainforest in the Amazon biome that covers most of the Amazon basin of South America.",
},
{
id: 4,
title: "The Arctic",
preview: "The Arctic is a polar region located at the northernmost part of Earth. The Arctic consists of the Arctic Ocean, adjacent seas, and parts of Alaska (United States), Northern Canada (Canada), Finland, Greenland (Kingdom of Denmark), Iceland, Norway, Russia, and Sweden.",
},
{
id: 5,
title: "The Antarctic",
preview: "The Antarctic, is a polar region containing the geographic South Pole and is situated in the Antarctic region of the Southern Hemisphere, almost entirely south of the Antarctic Circle, and is surrounded by the Southern Ocean.",
}
];
useEffect(() => {
fetch('/api/articles')
// .then(response => response.json())
.then(_ => articles)
.then((data: ArticlePreview[]) => setArticlePreviews(data))
.catch(_ => setError('Failed to fetch articles'))
.finally(() => setLoading(false));
}, []);
return (
<div className="row" id="articles" 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>
<div style={{width: '100%', display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap'}}>
{articlePreviews.map(articlePreview => (
<ArticleCard key={articlePreview.id} articlePreview={articlePreview} />
))}
</div>
{!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>
)
}

View file

@ -0,0 +1,36 @@
import { ReactNode, MouseEventHandler } from 'react';
interface ButtonProps {
color: 'primary' | 'secondary';
children: ReactNode;
url?: string;
}
export default function Button({ color, children, url }: ButtonProps) {
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: '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',
textDecoration: 'none',
}} href={url} className="ni-button" role="button">{children}</a>
)
}

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

@ -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:"white",
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>)
}

View file

@ -0,0 +1,21 @@
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: "column", margin:"5px", width:"10%"}}><p>ENSIBS</p><p>RedCRAB</p></div>
<p>Made with by RedCRAB</p>
<div style={{ display : "flex", alignItems: "center", flexDirection: "row", margin:"5px", width:"10%"}}>
<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: "100%", height: "auto",
padding: "10px"}}/>
</div>
</footer>
)
}

View file

@ -0,0 +1,40 @@
import Button from "./Button";
import ButtonLink from "./ButtonLink";
import NavBar from "./NavBar";
export default function FstSection () {
return (
<div style={{
textAlign: "center",
color: "yellow",
backgroundImage: "url('/pictures/sea.gif')",
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
style={{
fontSize: "3em",
fontWeight: "bold",
textShadow: "2px 2px 4px #000000",
fontFamily: "Helvetica",
}}
>Help us to save our oceans</h1><br />
<div style={{display:"flex", justifyContent: "center", gap: "20px"}}>
<Button color="primary" children={"Aller au jeu"} url="/game" />
<Button color="secondary" children={"Lire les articles"} url="/" />
</div>
</div></div>
)
}

View 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>
)
}

View file

@ -0,0 +1,78 @@
import React, { useState } from "react";
interface ModalProps {
state: "none" | "flex",
setState: (state: "none" | "flex") => void
question : {
question: string,
reponse_idx: number,
choix: string[]
},
valides?: [boolean[], React.Dispatch<React.SetStateAction<boolean[]>>],
markerIndex?: number
}
export default function Modal({ state, setState, question, valides, markerIndex }: ModalProps) {
const [isSuccess, setIsSuccess] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
console.log(state, setState, question)
return (
<div style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "center",
alignItems: "center",
display: state
}}>
<div style={{
width: "40%",
height: "40%",
backgroundColor: "white",
borderRadius: 10,
padding: 20
}}>
{
isSuccess ? <p>CONGRATULATIONS</p> : isSubmitted && !isSuccess && <p>WRONG : CORRECT ANSWER IS {question.choix[question.reponse_idx]}</p>
}
<h2>{question.question}</h2>
<div style={{ display: "flex", flexDirection: "column" }}>
{
question.choix.map((choix, index) => (
<button key={index} onClick={() => {
setIsSubmitted(true);
if (index === question.reponse_idx) {
console.log("Correct")
setIsSuccess(true);
const newValides = [...valides![0]];
newValides[markerIndex!] = true;
valides![1](newValides);
} else {
console.log("Wrong")
setIsSuccess(false);
}
}}
style={{
backgroundColor: isSubmitted && index === question.reponse_idx ? "green" : isSubmitted && index !== question.reponse_idx ? "red" : "white",
color: isSubmitted && index === question.reponse_idx ? "white" : "black"
}}
disabled={isSubmitted}
>{choix}</button>
))
}
<div/>
<button onClick={() => {
setIsSubmitted(false);
setIsSuccess(false);
setState("none");
}}>Close</button>
</div>
</div>
</div>
)}

View file

@ -0,0 +1,19 @@
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", padding: "0.5em 1em"}}>
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>
{/* <LogoButton url="/" logo = "https://archlinux.org/static/hetzner_logo.41114a37d25f.png"/> */}
<span class="material-symbols-outlined"
style={{fontSize: "4em", color: "white", margin: "0.5em"}}>sailing</span>
<ClickableLink url="/" text = "Accueil" />
<ClickableLink url="/game" text = "Jeu" />
</div>
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>
<RoundButton url="https://archlinux.org" bgcolor="var(--color-white)" text="?"/>
</div>
</nav>);
}

View file

@ -0,0 +1,35 @@
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,
borderStyle: 'solid',
borderColor:"var(--color-lightblue)",
borderRadius: '50%',
width: '45px',
height: '45px',
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
View 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;
}

View file

@ -4,13 +4,21 @@ import MainPage from "./pages/MainPage.tsx";
import GamePage from "./pages/GamePage.tsx"; import GamePage from "./pages/GamePage.tsx";
import ChaosPage from "./pages/ChaosPage.tsx"; import ChaosPage from "./pages/ChaosPage.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 />} />
<Route path="/chaos" element={<ChaosPage />} /> <Route path="/chaos" element={<ChaosPage />} />
// 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,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;

View file

@ -1,9 +1,154 @@
import { Canvas } from "@react-three/fiber";
import { OrbitControls, PerspectiveCamera, Sky } from "@react-three/drei";
import * as THREE from "three";
import React, { useEffect, useRef, useState } from "react";
import Ocean from "../components/3d/Ocean";
import Axes from "../components/3d/Axes";
import Character from "../components/3d/Character";
import Floor from "../components/3d/Floor";
import Marker from "../components/3d/Marker";
import Modal from "../components/Modal";
import { randInt } from "three/src/math/MathUtils.js";
export default function GamePage() { export default function GamePage() {
const [stateModal, setStateModal] = useState("none" as "none" | "flex");
const [questionIndex, setQuestionIndex] = useState(0);
const QuestionList = [
// Ensemble de questions sur la polution des océans
// {question:string, reponse_idx:int, choix:[string, string, string, string]}
{
question: "Quel est le plus grand océan du monde ?",
reponse_idx: 1,
choix: ["Océan Atlantique", "Océan Pacifique", "Océan Indien", "Océan Arctique"]
},
{
question: "Comment s'appelle les continents de plastique dans les océans ?",
reponse_idx: 2,
choix: ["Plastico", "Plastiland", "Plastiques", "Plastiques"]
},
{
question: "Quel est le nom de la grande masse d'eau salée qui recouvre la majorité de la surface de la Terre ?",
reponse_idx: 3,
choix: ["Mer", "Lac", "Océan", "Rivière"]
},
{
question: "Quel est l'océan le plus froid ?",
reponse_idx: 0,
choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"]
},
{
question: "Quel est l'océan le plus chaud ?",
reponse_idx: 1,
choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"]
},
{
question: "Quel est l'océan le plus profond ?",
reponse_idx: 1,
choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"]
},
{
question: "Quel est l'océan le plus grand ?",
reponse_idx: 2,
choix: ["Océan Arctique", "Océan Antarctique", "Océan Pacifique", "Océan Atlantique"]
}
]
const [valides, setValides] = useState([false, false, false, false, false, false]);
const [markerIndex, setMarkerIndex] = useState(0);
const askQuestion = (markerIndex: number) => {
setQuestionIndex(randInt(0, QuestionList.length - 1));
setStateModal("flex");
setMarkerIndex(markerIndex);
}
const MakerList = [
{ position: [0, -0.1, 0], onClick: () => askQuestion(0) }, // body
{ position: [-0.5, -0.1, 2.5], onClick: () => askQuestion(1) }, // left foot
{ position: [0.5, -0.1, 2.5], onClick: () => askQuestion(2) }, // right foot
{ position: [-1, -0.1, 1], onClick: () => askQuestion(3) }, // left hand
{ position: [1, -0.1, 1], onClick: () => askQuestion(4) }, // right hand
{ position: [0, -0.1, -1], onClick: () => askQuestion(5) }, // head
]
// Win condition
useEffect(() => {
if (valides.every(valide => valide)) {
}
}, [valides])
console.log(stateModal, setStateModal);
return ( return (
<div> <div id="canvas-container"
<h1>Game Page</h1> style={{
width: "100%",
height: "100%",
position: "fixed",
top: 0,
left: 0,
zIndex: -1
}}
>
{ !valides.every(valide => valide) === true ?
<p
style={{
position: "relative",
top: 0,
left: 0,
width: "100%",
height: "5%",
backgroundColor: "rgba(0, 0, 0, 1)",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
margin: 0
}}
> <strong>Hint : </strong> Répondez correctement aux questions pour sauver le personnage</p>
: <p
style={{
position: "relative",
top: 0,
left: 0,
width: "100%",
height: "5%",
backgroundColor: "rgba(0, 0, 0, 1)",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
margin: 0
}}
> <strong>Good Game : </strong> Bravo, vous avez sauvé le personnage</p>
}
<Canvas>
<Sky sunPosition={[100, 10, 100]} />
<OrbitControls />
<Ocean />
{/* <Axes /> */}
<Character />
<Floor />
{/* <Marker position={[0, 1, 0]} color="red" /> */}
{MakerList.map((marker, index) => (
<Marker key={index} position={marker.position} color={valides[index] ? "green" : "red"} onClick={marker.onClick} />
))}
<PerspectiveCamera makeDefault position={[0, 10, 3]} />
</Canvas>
<Modal state={stateModal} setState={setStateModal} question={QuestionList[questionIndex]} valides={[valides, setValides]} markerIndex={markerIndex} />
</div> </div>
) )
} }

View file

@ -1,10 +1,15 @@
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 />
<ArticlesSection />
<Footer bgcolor="lightblue"/>
</div> </div>
</>
) )
} }

13
front/src/types.ts Normal file
View 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 };

View file

@ -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
View file

@ -0,0 +1,6 @@
{
"name": "projet-nuitinfo-2024",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}