generated from lucien/actix-react-template
Compare commits
27 commits
Author | SHA1 | Date | |
---|---|---|---|
1d207b56a6 | |||
6228e839d7 | |||
![]() |
75c29c719a | ||
![]() |
2b71492587 | ||
![]() |
0508ddaa7f | ||
![]() |
b2cb9ca5c8 | ||
![]() |
1e95438b2c | ||
![]() |
0fa1a9b6f3 | ||
![]() |
6405c6db09 | ||
![]() |
f8ffe3e16f | ||
![]() |
d685e00bc3 | ||
![]() |
3f9617549d | ||
![]() |
0e31e90af9 | ||
![]() |
6e77233ac0 | ||
1ec6b1b0da | |||
![]() |
4682f8a61e | ||
b8fa053ca3 | |||
![]() |
53853dcce5 | ||
![]() |
ea2bb7ac3c | ||
![]() |
e1bcb4b022 | ||
![]() |
cbae2746fd | ||
b252122272 | |||
467d560b15 | |||
ac3fc5c391 | |||
d6f2d6cc09 | |||
e42b81539f | |||
ec2322f04c |
19 changed files with 289 additions and 42 deletions
|
@ -12,7 +12,9 @@ pub fn init() -> sqlite::Result<()> {
|
|||
"CREATE TABLE IF NOT EXISTS articles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
subTitle TEXT,
|
||||
auteur TEXT,
|
||||
edited_at DATE_FORMAT('now', '%YYYY-%mm-%dd'),
|
||||
published_at DATE_FORMAT('now', '%YYYY-%mm-%dd'),
|
||||
content TEXT NOT NULL
|
||||
)",
|
||||
)?;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
mod create_db;
|
||||
use create_db::init;
|
||||
|
||||
use actix_web::{App, HttpServer, get, Responder, HttpResponse, http::header::ContentType};
|
||||
use actix_web::{App, web, HttpServer, get, Responder, HttpResponse, http::header::ContentType};
|
||||
use actix_files::Files;
|
||||
use serde_json::json;
|
||||
use sqlite::{Connection, State};
|
||||
|
@ -11,9 +11,9 @@ use serde::{Serialize, Deserialize};
|
|||
struct Article {
|
||||
id: i64,
|
||||
title: String,
|
||||
auteur: Option<String>,
|
||||
edited_at: Option<String>,
|
||||
published_at: Option<String>,
|
||||
auteur: String,
|
||||
edited_at: String,
|
||||
published_at: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
|
@ -31,13 +31,13 @@ async fn get_articles() -> impl Responder {
|
|||
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, _>(3).unwrap();
|
||||
let content = stmt.read::<String, _>(5).unwrap();
|
||||
articles.push(Article {
|
||||
id,
|
||||
title,
|
||||
auteur: None,
|
||||
edited_at: None,
|
||||
published_at: None,
|
||||
auteur: "".to_string(),
|
||||
edited_at: "".to_string(),
|
||||
published_at: "".to_string(),
|
||||
content
|
||||
});
|
||||
}
|
||||
|
@ -45,6 +45,54 @@ async fn get_articles() -> impl Responder {
|
|||
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!({
|
||||
|
@ -72,7 +120,10 @@ async fn main() -> Result<(), std::io::Error> {
|
|||
.service(hello)
|
||||
.service(get_articles)
|
||||
.service(api)
|
||||
.service(get_article)
|
||||
.service(Files::new("/", "public").index_file("index.html"))
|
||||
.service(Files::new("/game", "public").index_file("index.html"))
|
||||
.service(Files::new("/chaos", "public").index_file("index.html"))
|
||||
})
|
||||
.bind(("0.0.0.0", 2486))?
|
||||
.run()
|
||||
|
|
|
@ -3,10 +3,22 @@ services:
|
|||
build:
|
||||
context: .
|
||||
dockerfile: dockerfile
|
||||
container_name: web
|
||||
network: host
|
||||
container_name: nuitdelinfo
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./back/data:/app/data
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.nuitdelinfo.rule=Host(`nuitdelinfo.leizour.fr`)"
|
||||
- "traefik.http.routers.nuitdelinfo.entrypoints=websecure"
|
||||
- "traefik.http.routers.nuitdelinfo.tls=true"
|
||||
- "traefik.http.routers.nuitdelinfo.tls.certresolver=myresolver"
|
||||
- "traefik.http.services.nuitdelinfo.loadbalancer.server.port=2486"
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ RUN cargo build --release
|
|||
|
||||
FROM debian:bookworm-slim
|
||||
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=back /app/target/release/back /app/back
|
||||
EXPOSE 8080
|
||||
EXPOSE 2486
|
||||
CMD ["/app/back"]
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^7.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"three": "^0.171.0",
|
||||
"three-stdlib": "^2.34.0"
|
||||
"three-stdlib": "^2.34.0",
|
||||
"react-router-dom": "^6.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
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() {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
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')
|
||||
const { scene } = useGLTF('/models/man.glb')
|
||||
|
||||
// rotate the character
|
||||
scene.rotation.x = -Math.PI / 2
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
import React from 'react'
|
||||
import { Group } from 'three'
|
||||
import * as THREE from 'three'
|
||||
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import React from 'react'
|
||||
import { Group } from 'three'
|
||||
import * as THREE from 'three'
|
||||
|
||||
interface MarkerProps {
|
||||
position: [number, number, number],
|
||||
position: number[],
|
||||
color: string,
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export default function Marker({ position, color, onClick }: MarkerProps) {
|
||||
|
||||
const [positionState, setPositionState] = React.useState(position)
|
||||
const [positionState, setPositionState] = React.useState(new THREE.Vector3(...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)}>
|
||||
<mesh position={positionState} rotation={[Math.PI,0,0]} onClick={onClick} onPointerOver={() => setPositionState(positionState.clone().setZ(positionState.z + 0.1))} onPointerOut={() => setPositionState(new THREE.Vector3(...position))}>
|
||||
<coneGeometry args={[0.15, 0.6, 6]} />
|
||||
<meshStandardMaterial color={color} side={THREE.DoubleSide} />
|
||||
</mesh>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
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 = () => {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ReactNode, MouseEventHandler } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ButtonProps {
|
||||
color: 'primary' | 'secondary';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Button from "./Button";
|
||||
import ButtonLink from "./ButtonLink";
|
||||
|
||||
import NavBar from "./NavBar";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export default function FstSection () {
|
||||
return (
|
||||
|
@ -32,8 +32,55 @@ export default function FstSection () {
|
|||
|
||||
>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="/" />
|
||||
{/* <Button color="primary" children={"Aller au jeu"} url="/game" />
|
||||
<Button color="secondary" children={"Lire les articles"} url="/" /> */}
|
||||
<Link to="/game" style={{
|
||||
backgroundColor: 'var(--color-yellow)',
|
||||
borderRadius: '8px',
|
||||
borderWidth: '0',
|
||||
color: 'var(--color-black)',
|
||||
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',
|
||||
}}>Aller au jeu</Link>
|
||||
<Link to="/articles" style={{
|
||||
backgroundColor: 'var(--color-darkblue)',
|
||||
borderRadius: '8px',
|
||||
borderWidth: '0',
|
||||
color: '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',
|
||||
}}>Lire les articles</Link>
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import LogoButton from '../components/LogoButton.tsx'
|
||||
import ClickableLink from './ClickableLink.tsx';
|
||||
import { Link } from 'react-router';
|
||||
import RoundButton from './RoundButton.tsx';
|
||||
|
||||
export default function NavBar(){
|
||||
|
@ -7,13 +6,15 @@ export default function NavBar(){
|
|||
<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"
|
||||
<span className="material-symbols-outlined"
|
||||
style={{fontSize: "4em", color: "white", margin: "0.5em"}}>sailing</span>
|
||||
<ClickableLink url="/" text = "Accueil" />
|
||||
<ClickableLink url="/game" text = "Jeu" />
|
||||
<Link to="/" style={{ textDecoration: "none", color: "white", fontSize: "1.5em", margin: "0.5em", fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif'}}>Home</Link>
|
||||
<Link to="/game" style={{ textDecoration: "none", color: "white", fontSize: "1.5em", margin: "0.5em", fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif'}}>Game</Link>
|
||||
<Link to="/articles" style={{ textDecoration: "none", color: "white", fontSize: "1.5em", margin: "0.5em", fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif'}}>Articles</Link>
|
||||
<Link to="/chaos" style={{ textDecoration: "none", color: "white", fontSize: "1.5em", margin: "0.5em", fontFamily: '"Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif'}}>Le Chaos</Link>
|
||||
</div>
|
||||
<div style={{ display : "flex", alignItems: "center", flexDirection: "row"}}>
|
||||
<RoundButton url="https://archlinux.org" bgcolor="var(--color-white)" text="?"/>
|
||||
<RoundButton url="https://www.apple.com/macos/macos-sequoia/" bgcolor="var(--color-white)" text="?"/>
|
||||
</div>
|
||||
</nav>);
|
||||
}
|
16
front/src/components/chaos/MonInput.tsx
Normal file
16
front/src/components/chaos/MonInput.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
//import { useState } from "react";
|
||||
|
||||
interface MonInputProps {
|
||||
text: string;
|
||||
new_focus: () => void;
|
||||
police: string;
|
||||
}
|
||||
|
||||
export default function MonInput({text, new_focus, police}: MonInputProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input readOnly value={text} onFocus={new_focus} style={{fontFamily: police,width:1000}}></input>
|
||||
</div>
|
||||
)
|
||||
}
|
26
front/src/components/chaos/monButton.tsx
Normal file
26
front/src/components/chaos/monButton.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
|
||||
interface MonButtonProps {
|
||||
letter: string;
|
||||
changetext: (arg0: string) => void;
|
||||
sizeFrontw: number;
|
||||
sizeFronth: number;
|
||||
rdmFront: () => void;
|
||||
color: string;
|
||||
}
|
||||
|
||||
|
||||
export default function MonButton({letter,changetext,sizeFrontw,sizeFronth,rdmFront,color}: MonButtonProps) {
|
||||
|
||||
|
||||
|
||||
function clicked() {rdmFront();
|
||||
changetext(letter)}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className="key" onClick={clicked} style={{width:sizeFrontw,height:sizeFronth,background:color}}>{letter}</button>
|
||||
</div>
|
||||
)
|
||||
}
|
12
front/src/components/chaos/style.css
Normal file
12
front/src/components/chaos/style.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
#keys {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.key {
|
||||
font-size: 10px;
|
||||
border: solid black;
|
||||
}
|
|
@ -2,6 +2,8 @@ import { BrowserRouter, Route, Routes } from "react-router";
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import MainPage from "./pages/MainPage.tsx";
|
||||
import GamePage from "./pages/GamePage.tsx";
|
||||
import ChaosPage from "./pages/ChaosPage.tsx";
|
||||
|
||||
import ArticlePage from "./pages/ArticlePage.tsx";
|
||||
import './index.css'
|
||||
|
||||
|
@ -12,6 +14,7 @@ createRoot(document.getElementById('root')!).render(
|
|||
<Route path="/" element={<MainPage />} />
|
||||
// Game page
|
||||
<Route path="/game" element={<GamePage />} />
|
||||
<Route path="/chaos" element={<ChaosPage />} />
|
||||
// Article page (dynamic route)
|
||||
<Route path="/article/:id" element={<ArticlePage />} />
|
||||
// Not found
|
||||
|
|
86
front/src/pages/ChaosPage.tsx
Normal file
86
front/src/pages/ChaosPage.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import MonButton from "../components/chaos/monButton";
|
||||
import MonInput from "../components/chaos/MonInput";
|
||||
import "../components/chaos/style.css"
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
export default function ChaosPage(){
|
||||
|
||||
const [array_letter,setArray_letter]=useState(["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"," ","!"]);
|
||||
|
||||
const [sizeFrontw,setSizeFrontw] = useState(16)
|
||||
const [sizeFronth,setSizeFronth] = useState(20)
|
||||
const [color,setColor] = useState("#ffffff")
|
||||
|
||||
function randomFront(){setSizeFronth(Math.floor(Math.random() * (1000)));
|
||||
setSizeFrontw(Math.floor(Math.random() * (1000)));
|
||||
setColor(`#${Math.floor(Math.random() * 16777215).toString(16)}`)}
|
||||
|
||||
function shuffleArray(arr: string[]) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1)); // Choisir un index aléatoire
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]]; // Échanger les éléments
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
// console.log(shuffledArray); // Affiche un tableau mélangé
|
||||
|
||||
|
||||
const [entry1,setEntry1] = useState("")
|
||||
function E1(ent:string) {
|
||||
setEntry1(entry1+ent);
|
||||
}
|
||||
const [connarddefocus,setFocus] = useState(()=>E1)
|
||||
|
||||
const [entry2,setEntry2] = useState("")
|
||||
function E2(ent:string) {
|
||||
setEntry2(entry2+ent);
|
||||
}
|
||||
|
||||
function changeFocus(E: (ent:string)=>void) {
|
||||
setArray_letter(shuffleArray(array_letter));
|
||||
setFocus(()=>E);
|
||||
}
|
||||
|
||||
const [entry3,setEntry3] = useState("")
|
||||
function E3(ent:string) {
|
||||
setEntry3(entry3+ent);
|
||||
}
|
||||
|
||||
const [tel,setTel] = useState(0)
|
||||
|
||||
|
||||
return(
|
||||
<div>
|
||||
<h1>Chaos Page</h1>
|
||||
<p>Quel est votre nom ?</p>
|
||||
<MonInput text={entry1} new_focus={()=>changeFocus(E1)} police={""}/>
|
||||
<p>Quel adjectif désigne le mieux Xi Junpin ?</p>
|
||||
<MonInput text={entry2} new_focus={()=>changeFocus(E2)} police={""}/>
|
||||
<p>Combien font 1+1 ?</p>
|
||||
<MonInput text={entry3} new_focus={()=>changeFocus(E3)} police={"Wingdings"}/>
|
||||
<div id = "keys">
|
||||
{array_letter.map((letter) => {return <MonButton
|
||||
letter={letter}
|
||||
changetext={connarddefocus}
|
||||
sizeFrontw={sizeFrontw}
|
||||
sizeFronth={sizeFronth}
|
||||
rdmFront={randomFront}
|
||||
color={color}/> })}
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>Formulaire super mega bien</legend>
|
||||
<input type="text" placeholder="nom"/>
|
||||
<input type="text" placeholder="prenom"/>
|
||||
<input type="text" placeholder="email"/>
|
||||
<input type="text" placeholder="mdp"/>
|
||||
<input type="text" placeholder="mdp2"/>
|
||||
<label htmlFor="phone">phone : {tel}</label>
|
||||
<input type="range" min="0" max="9999999999" step="1" name="phone" id="phone" value={tel} onChange={(e)=>setTel(parseInt(e.target.value))}/>
|
||||
<input type="submit" value="envoyer"/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
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 { useEffect, 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";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue