first alpha version
Browse files- .gitignore +24 -0
- README.md +9 -11
- index.html +13 -18
- package-lock.json +0 -0
- package.json +25 -0
- public/favicon.svg +40 -0
- src/app.tsx +202 -0
- src/components/button/index.tsx +37 -0
- src/components/button/style.module.scss +105 -0
- src/components/container/index.tsx +10 -0
- src/components/container/style.module.scss +6 -0
- src/components/form/index.tsx +12 -0
- src/components/form/style.module.scss +10 -0
- src/components/input/index.tsx +86 -0
- src/components/input/style.module.scss +45 -0
- src/components/layout/index.tsx +29 -0
- src/components/layout/style.module.scss +25 -0
- src/components/preview/index.tsx +161 -0
- src/components/preview/style.module.scss +103 -0
- src/components/settings/index.tsx +51 -0
- src/components/settings/style.module.scss +0 -0
- src/components/slider/index.tsx +60 -0
- src/components/slider/style.module.scss +330 -0
- src/components/spinner/index.tsx +12 -0
- src/components/spinner/style.module.scss +46 -0
- src/components/topic/index.tsx +65 -0
- src/components/topic/style.module.scss +51 -0
- src/components/topics/index.tsx +107 -0
- src/components/topics/style.module.scss +71 -0
- src/index.ts +4 -0
- src/reset.scss +115 -0
- src/style.module.scss +13 -0
- src/styles.scss +139 -0
- src/types.d.ts +0 -0
- src/utils/dates.ts +51 -0
- src/utils/model.ts +107 -0
- src/utils/oobabooga.ts +176 -0
- src/utils/route.ts +9 -0
- src/utils/settings.ts +26 -0
- src/utils/smileys.ts +79 -0
- src/utils/topics.ts +135 -0
- src/utils/uuids.ts +5 -0
- style.css +0 -28
- tsconfig.json +21 -0
- vite.config.ts +18 -0
.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
README.md
CHANGED
@@ -1,11 +1,9 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
+
# JVCGPT
|
2 |
+
|
3 |
+
## Getting Started
|
4 |
+
|
5 |
+
- `npm run dev` - Starts a dev server at http://localhost:5173/
|
6 |
+
|
7 |
+
- `npm run build` - Builds for production, emitting to `dist/`
|
8 |
+
|
9 |
+
- `npm run preview` - Starts a server at http://localhost:4173/ to test production build locally
|
|
|
|
index.html
CHANGED
@@ -1,19 +1,14 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
Also don't forget to check the
|
15 |
-
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
|
16 |
-
</p>
|
17 |
-
</div>
|
18 |
-
</body>
|
19 |
</html>
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="fr">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8"/>
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
7 |
+
<meta name="color-scheme" content="dark"/>
|
8 |
+
<title>JVCGPT</title>
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div id="app"></div>
|
12 |
+
<script type="module" src="/src/index.ts"></script>
|
13 |
+
</body>
|
|
|
|
|
|
|
|
|
|
|
14 |
</html>
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"private": true,
|
3 |
+
"type": "module",
|
4 |
+
"scripts": {
|
5 |
+
"dev": "vite",
|
6 |
+
"build": "vite build",
|
7 |
+
"preview": "vite preview"
|
8 |
+
},
|
9 |
+
"dependencies": {
|
10 |
+
"classnames": "^2.5.1",
|
11 |
+
"preact": "^10.25.3",
|
12 |
+
"preact-feather": "^4.2.1"
|
13 |
+
},
|
14 |
+
"devDependencies": {
|
15 |
+
"@preact/preset-vite": "^2.9.3",
|
16 |
+
"eslint": "^8.57.1",
|
17 |
+
"eslint-config-preact": "^1.5.0",
|
18 |
+
"sass-embedded": "^1.83.4",
|
19 |
+
"typescript": "^5.7.3",
|
20 |
+
"vite": "^6.0.4"
|
21 |
+
},
|
22 |
+
"eslintConfig": {
|
23 |
+
"extends": "preact"
|
24 |
+
}
|
25 |
+
}
|
public/favicon.svg
ADDED
|
src/app.tsx
ADDED
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {JSX} from "preact";
|
2 |
+
import {useState, useEffect, useCallback} from "preact/hooks";
|
3 |
+
import "./styles.scss";
|
4 |
+
import style from "./style.module.scss";
|
5 |
+
import {Container} from "./components/container";
|
6 |
+
import {loadTopics, saveTopics} from "./utils/topics";
|
7 |
+
import {Topic} from "./utils/topics";
|
8 |
+
import {Topics} from "./components/topics";
|
9 |
+
import {Topic as TopicComponent} from "./components/topic";
|
10 |
+
import {Route, routes, RouteSetter} from "./utils/route";
|
11 |
+
import {Settings} from "./components/settings";
|
12 |
+
import {Layout} from "./components/layout";
|
13 |
+
import {fetchSettings, saveSettings} from "./utils/settings";
|
14 |
+
import {generateTopic, generatePosts} from "./utils/oobabooga";
|
15 |
+
import {tokenizeTopic} from "./utils/model";
|
16 |
+
|
17 |
+
export function App(): JSX.Element {
|
18 |
+
const [route, _setRoute] = useState<Route>(routes.home);
|
19 |
+
const [page, setPage] = useState<number>(0);
|
20 |
+
const [topicId, setTopicId] = useState<Topic["id"] | null>(null);
|
21 |
+
|
22 |
+
// null when loading
|
23 |
+
const [topics, setTopics] = useState<Topic[]>(loadTopics);
|
24 |
+
const [latestGeneratedTopicId, setLatestGeneratedTopicId] = useState<string|null>(null);
|
25 |
+
const [pendingGeneration, setPendingGeneration] = useState<boolean>(false);
|
26 |
+
|
27 |
+
useEffect(() => {
|
28 |
+
console.log("save !")
|
29 |
+
saveTopics(topics);
|
30 |
+
}, [topics]);
|
31 |
+
|
32 |
+
const _generateTopic = async (postsCount: number) => {
|
33 |
+
setPendingGeneration(true);
|
34 |
+
const topic = await generateTopic(settings, postsCount);
|
35 |
+
setLatestGeneratedTopicId(topic.id);
|
36 |
+
setTopics(topics => [...topics, topic]);
|
37 |
+
setPendingGeneration(false);
|
38 |
+
}
|
39 |
+
|
40 |
+
const addPosts = async (topicId: string, postsCount: number) => {
|
41 |
+
setPendingGeneration(true);
|
42 |
+
const posts = await generatePosts(settings, postsCount, topics.find(t => t.id === topicId));
|
43 |
+
const newTopics = [...topics]; // Clone topics to avoid bugs
|
44 |
+
const foundIndex = newTopics.findIndex(t => t.id === topicId);
|
45 |
+
newTopics[foundIndex].posts = newTopics[foundIndex].posts.concat(posts);
|
46 |
+
setTopics(newTopics)
|
47 |
+
setPendingGeneration(false);
|
48 |
+
}
|
49 |
+
|
50 |
+
// useEffect(() => {
|
51 |
+
// setTopics(loadTopics());
|
52 |
+
// }, []);
|
53 |
+
|
54 |
+
const [settings, setSettings] = useState(fetchSettings)
|
55 |
+
|
56 |
+
useEffect(() => {
|
57 |
+
saveSettings(settings);
|
58 |
+
}, [settings]);
|
59 |
+
|
60 |
+
const updateRoute = useCallback(() => {
|
61 |
+
const url = new URL(window.location.href);
|
62 |
+
const route = url.searchParams.get("route");
|
63 |
+
if (route && route in routes) {
|
64 |
+
_setRoute(route as keyof typeof routes);
|
65 |
+
}
|
66 |
+
const page = url.searchParams.get("page");
|
67 |
+
if (page) {
|
68 |
+
setPage(parseInt(page));
|
69 |
+
}
|
70 |
+
const id = url.searchParams.get("id");
|
71 |
+
if (id) {
|
72 |
+
setTopicId(id);
|
73 |
+
}
|
74 |
+
}, []);
|
75 |
+
|
76 |
+
// Init page from url
|
77 |
+
useEffect(() => {
|
78 |
+
updateRoute()
|
79 |
+
}, []);
|
80 |
+
|
81 |
+
// Listen for URl change
|
82 |
+
useEffect(() => {
|
83 |
+
function listener() {
|
84 |
+
updateRoute()
|
85 |
+
}
|
86 |
+
|
87 |
+
window.addEventListener('popstate', listener);
|
88 |
+
|
89 |
+
return () => {
|
90 |
+
window.removeEventListener('popstate', listener)
|
91 |
+
}
|
92 |
+
}, []);
|
93 |
+
|
94 |
+
|
95 |
+
// Update URL params on route change
|
96 |
+
const setRoute: RouteSetter = useCallback((route: Route, page?: number, id?: string) => {
|
97 |
+
const url = new URL(window.location.href);
|
98 |
+
url.searchParams.set("route", route);
|
99 |
+
_setRoute(route);
|
100 |
+
|
101 |
+
if (page !== undefined) {
|
102 |
+
url.searchParams.set("page", String(page));
|
103 |
+
setPage(page);
|
104 |
+
} else {
|
105 |
+
url.searchParams.delete("page");
|
106 |
+
setPage(0);
|
107 |
+
}
|
108 |
+
|
109 |
+
if (id !== undefined) {
|
110 |
+
url.searchParams.set("id", id);
|
111 |
+
setTopicId(id);
|
112 |
+
} else {
|
113 |
+
url.searchParams.delete("id");
|
114 |
+
setTopicId(null);
|
115 |
+
}
|
116 |
+
|
117 |
+
const newUrl = url.toString();
|
118 |
+
|
119 |
+
// Avoid to push the same URL mutiple time
|
120 |
+
if (newUrl !== window.location.href) {
|
121 |
+
window.history.pushState({}, "", newUrl);
|
122 |
+
}
|
123 |
+
}, []);
|
124 |
+
|
125 |
+
let routeComponent: JSX.Element = undefined;
|
126 |
+
let breadcrumbs: string = undefined;
|
127 |
+
let title: string = undefined;
|
128 |
+
|
129 |
+
switch (route) {
|
130 |
+
case routes.home:
|
131 |
+
routeComponent = <Topics
|
132 |
+
topics={topics}
|
133 |
+
setRoute={setRoute}
|
134 |
+
settings={settings}
|
135 |
+
setSettings={setSettings}
|
136 |
+
generateTopic={_generateTopic}
|
137 |
+
pendingGeneration={pendingGeneration}
|
138 |
+
latestGeneratedTopicId={latestGeneratedTopicId}
|
139 |
+
/>
|
140 |
+
breadcrumbs = "accueil"
|
141 |
+
title = "Liste des topics"
|
142 |
+
break;
|
143 |
+
case routes.topic:
|
144 |
+
if (topicId === null) {
|
145 |
+
routeComponent = <div>Impossible d'afficher le topic</div>
|
146 |
+
breadcrumbs = "accueil"
|
147 |
+
title = "Topic"
|
148 |
+
} else {
|
149 |
+
if (topics === null) {
|
150 |
+
routeComponent = <div>Chargement...</div>
|
151 |
+
breadcrumbs = `accueil / topic`
|
152 |
+
title = `Chargement...`
|
153 |
+
} else {
|
154 |
+
const topic = topics.find(t => t.id === topicId);
|
155 |
+
routeComponent = <TopicComponent
|
156 |
+
topic={topic}
|
157 |
+
settings={settings}
|
158 |
+
setSettings={setSettings}
|
159 |
+
addPosts={addPosts}
|
160 |
+
pendingGeneration={pendingGeneration}
|
161 |
+
/>
|
162 |
+
breadcrumbs = `accueil / ${topic.title}`
|
163 |
+
title = `Topic : ${topic.title}`
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
break;
|
168 |
+
case routes.settings:
|
169 |
+
routeComponent = <Settings settings={settings} setSettings={setSettings}/>
|
170 |
+
breadcrumbs = "accueil / paramètres"
|
171 |
+
title = "Paramètres"
|
172 |
+
break;
|
173 |
+
}
|
174 |
+
|
175 |
+
return (
|
176 |
+
<>
|
177 |
+
<header className={style.header}>
|
178 |
+
<Container>
|
179 |
+
<h1 className={style.logo}>
|
180 |
+
<a href="#" onClick={e => {
|
181 |
+
e.preventDefault();
|
182 |
+
setRoute(routes.home);
|
183 |
+
}}>
|
184 |
+
JVCGPT
|
185 |
+
</a>
|
186 |
+
</h1>
|
187 |
+
</Container>
|
188 |
+
</header>
|
189 |
+
<main>
|
190 |
+
<Container>
|
191 |
+
<Layout
|
192 |
+
breadcrumbs={breadcrumbs}
|
193 |
+
title={title}
|
194 |
+
setRoute={setRoute}
|
195 |
+
>
|
196 |
+
{routeComponent}
|
197 |
+
</Layout>
|
198 |
+
</Container>
|
199 |
+
</main>
|
200 |
+
</>
|
201 |
+
);
|
202 |
+
}
|
src/components/button/index.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {ComponentChildren, JSX} from 'preact';
|
2 |
+
import cn from 'classnames';
|
3 |
+
import style from "./style.module.scss";
|
4 |
+
|
5 |
+
export function Button(props: {
|
6 |
+
loading?: boolean;
|
7 |
+
onClick?: () => void;
|
8 |
+
className?: string;
|
9 |
+
secondary?: boolean;
|
10 |
+
disabled?: boolean;
|
11 |
+
children: ComponentChildren;
|
12 |
+
}) {
|
13 |
+
const disabled = props.disabled || props.loading;
|
14 |
+
const buttonClass = cn(style.btn, {[style.secondary]: props.secondary}, 'button', props.className, {[style.disabled]:disabled});
|
15 |
+
|
16 |
+
let spinner: JSX.Element = undefined;
|
17 |
+
|
18 |
+
if (props.loading) {
|
19 |
+
spinner = <span className={style.spinner}/>
|
20 |
+
}
|
21 |
+
|
22 |
+
return (
|
23 |
+
<button
|
24 |
+
type="button"
|
25 |
+
onClick={() => {
|
26 |
+
if (!disabled) {
|
27 |
+
props.onClick()
|
28 |
+
}
|
29 |
+
}}
|
30 |
+
className={buttonClass}
|
31 |
+
disabled={disabled}
|
32 |
+
>
|
33 |
+
{spinner}
|
34 |
+
{props.children}
|
35 |
+
</button>
|
36 |
+
);
|
37 |
+
}
|
src/components/button/style.module.scss
ADDED
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.btn {
|
2 |
+
position: relative;
|
3 |
+
display: inline-flex;
|
4 |
+
justify-content: center;
|
5 |
+
align-items: center;
|
6 |
+
padding: 0 1.25rem;
|
7 |
+
margin: 0;
|
8 |
+
border: 0.0625rem solid var(--text-secondary);
|
9 |
+
height: 2.25rem;
|
10 |
+
line-height: 2.25rem;
|
11 |
+
font-size: 0.9375rem;
|
12 |
+
text-decoration: none;
|
13 |
+
color: var(--text-secondary);
|
14 |
+
border-radius: 1.125rem;
|
15 |
+
|
16 |
+
white-space: nowrap;
|
17 |
+
cursor: pointer;
|
18 |
+
//overflow: hidden;
|
19 |
+
|
20 |
+
&:not(.disabled):hover {
|
21 |
+
border-color: var(--text-hover-secondary);
|
22 |
+
color: var(--text-hover-secondary);
|
23 |
+
}
|
24 |
+
//
|
25 |
+
//&:active {
|
26 |
+
// //border-color: var(--text-secondary);
|
27 |
+
// background: var(--text-secondary);
|
28 |
+
//}
|
29 |
+
|
30 |
+
&.secondary {
|
31 |
+
color: #fff;
|
32 |
+
background-color: var(--text-secondary);
|
33 |
+
|
34 |
+
&:not(.disabled):hover {
|
35 |
+
border-color: var(--text-hover-secondary);
|
36 |
+
background: var(--text-hover-secondary);
|
37 |
+
color: #fff;
|
38 |
+
}
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
.spinner {
|
43 |
+
font-size: 10px;
|
44 |
+
//margin: 50px auto;
|
45 |
+
margin-right: 1em;
|
46 |
+
width: 1.5em;
|
47 |
+
height: 1.5em;
|
48 |
+
border-radius: 50%;
|
49 |
+
background: #ffffff;
|
50 |
+
background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
|
51 |
+
position: relative;
|
52 |
+
animation: load3 1.4s infinite linear;
|
53 |
+
transform: translateZ(0);
|
54 |
+
|
55 |
+
&:before {
|
56 |
+
width: 50%;
|
57 |
+
height: 50%;
|
58 |
+
background: #ffffff;
|
59 |
+
border-radius: 100% 0 0 0;
|
60 |
+
position: absolute;
|
61 |
+
top: 0;
|
62 |
+
left: 0;
|
63 |
+
content: '';
|
64 |
+
}
|
65 |
+
|
66 |
+
&:after {
|
67 |
+
background: var(--text-secondary);
|
68 |
+
width: 75%;
|
69 |
+
height: 75%;
|
70 |
+
border-radius: 50%;
|
71 |
+
content: '';
|
72 |
+
margin: auto;
|
73 |
+
position: absolute;
|
74 |
+
top: 0;
|
75 |
+
left: 0;
|
76 |
+
bottom: 0;
|
77 |
+
right: 0;
|
78 |
+
}
|
79 |
+
}
|
80 |
+
|
81 |
+
@keyframes load3 {
|
82 |
+
0% {
|
83 |
+
transform: rotate(0deg);
|
84 |
+
}
|
85 |
+
100% {
|
86 |
+
transform: rotate(360deg);
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
90 |
+
.disabled {
|
91 |
+
cursor: not-allowed;
|
92 |
+
|
93 |
+
&:after {
|
94 |
+
content: "";
|
95 |
+
position: absolute;
|
96 |
+
width: 100%;
|
97 |
+
height: 100%;
|
98 |
+
background: #4f4f4f;
|
99 |
+
border: 0.0625rem solid #4f4f4f;
|
100 |
+
border-radius: 1.125rem;
|
101 |
+
opacity: 0.5;
|
102 |
+
box-sizing: content-box;
|
103 |
+
}
|
104 |
+
}
|
105 |
+
|
src/components/container/index.tsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type {ComponentChildren} from "preact";
|
2 |
+
import style from "./style.module.scss"
|
3 |
+
|
4 |
+
export function Container(props: { children: ComponentChildren }) {
|
5 |
+
return (
|
6 |
+
<div class={style.container}>
|
7 |
+
{props.children}
|
8 |
+
</div>
|
9 |
+
);
|
10 |
+
}
|
src/components/container/style.module.scss
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.container {
|
2 |
+
margin: 0 auto;
|
3 |
+
min-width: 20rem;
|
4 |
+
max-width: 73.5rem;
|
5 |
+
padding: 0 0.5rem;
|
6 |
+
}
|
src/components/form/index.tsx
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type {ComponentChildren} from "preact";
|
2 |
+
import style from "./style.module.scss"
|
3 |
+
|
4 |
+
export function FormGroup(props: {
|
5 |
+
children: ComponentChildren,
|
6 |
+
}) {
|
7 |
+
return (
|
8 |
+
<div className={style.formGroup}>
|
9 |
+
{props.children}
|
10 |
+
</div>
|
11 |
+
)
|
12 |
+
}
|
src/components/form/style.module.scss
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.formGroup {
|
2 |
+
display: flex;
|
3 |
+
flex-direction: column;
|
4 |
+
margin-bottom: 1rem;
|
5 |
+
|
6 |
+
label {
|
7 |
+
font-size: 1rem;
|
8 |
+
margin-bottom: 1rem;
|
9 |
+
}
|
10 |
+
}
|
src/components/input/index.tsx
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {FunctionalComponent} from "preact";
|
2 |
+
import {useState} from "preact/hooks";
|
3 |
+
import cn from "classnames";
|
4 |
+
import {JSX} from "preact";
|
5 |
+
// import GenericEventHandler = JSXInternal.GenericEventHandler;
|
6 |
+
import style from "./style.module.scss";
|
7 |
+
import {Settings} from "preact-feather";
|
8 |
+
import {FeatherProps} from "preact-feather/src/types";
|
9 |
+
|
10 |
+
interface InputProps<T> {
|
11 |
+
type: "text" | "number" | "email" | "password";
|
12 |
+
icon: FunctionalComponent<FeatherProps>;
|
13 |
+
value: T;
|
14 |
+
placeholder?: string;
|
15 |
+
onChange: (value: T) => void;
|
16 |
+
className?: string;
|
17 |
+
disabled?: boolean;
|
18 |
+
id?: string;
|
19 |
+
name?: string;
|
20 |
+
}
|
21 |
+
|
22 |
+
export const Input: FunctionalComponent<InputProps<string | number>> = ({
|
23 |
+
type,
|
24 |
+
icon,
|
25 |
+
value,
|
26 |
+
placeholder,
|
27 |
+
onChange,
|
28 |
+
className,
|
29 |
+
disabled,
|
30 |
+
id,
|
31 |
+
name,
|
32 |
+
}) => {
|
33 |
+
const [focused, setFocused] = useState(false);
|
34 |
+
|
35 |
+
const inputClass = cn(style.input, "generic-input", className, {
|
36 |
+
focused,
|
37 |
+
disabled,
|
38 |
+
});
|
39 |
+
|
40 |
+
const handleInputChange: JSX.GenericEventHandler<HTMLInputElement> = (event) => {
|
41 |
+
console.log("handleInputChange")
|
42 |
+
const target = event.target as HTMLInputElement;
|
43 |
+
onChange(type === "number" ? (parseFloat(target.value) || 0) : target.value);
|
44 |
+
};
|
45 |
+
|
46 |
+
const Icon = icon;
|
47 |
+
|
48 |
+
return (
|
49 |
+
<div className={style.wrapper}>
|
50 |
+
<Icon className={style.icon} size={18}/>
|
51 |
+
<input
|
52 |
+
// required=""
|
53 |
+
// minLength="3"
|
54 |
+
// maxLength="15"
|
55 |
+
title="Le pseudo doit avoir une longueur comprise entre 3 et 15 caractères."
|
56 |
+
// tabIndex="10"
|
57 |
+
type={type}
|
58 |
+
id={id}
|
59 |
+
name={name}
|
60 |
+
value={value}
|
61 |
+
placeholder={placeholder}
|
62 |
+
// onChange={handleInputChange}
|
63 |
+
onInput={handleInputChange}
|
64 |
+
className={inputClass}
|
65 |
+
disabled={disabled}
|
66 |
+
onFocus={() => setFocused(true)}
|
67 |
+
onBlur={() => setFocused(false)}
|
68 |
+
/>
|
69 |
+
</div>
|
70 |
+
)
|
71 |
+
|
72 |
+
return (
|
73 |
+
<input
|
74 |
+
type={type}
|
75 |
+
id={id}
|
76 |
+
name={name}
|
77 |
+
value={value}
|
78 |
+
placeholder={placeholder}
|
79 |
+
onChange={handleInputChange}
|
80 |
+
className={inputClass}
|
81 |
+
disabled={disabled}
|
82 |
+
onFocus={() => setFocused(true)}
|
83 |
+
onBlur={() => setFocused(false)}
|
84 |
+
/>
|
85 |
+
);
|
86 |
+
};
|
src/components/input/style.module.scss
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.wrapper {
|
2 |
+
position: relative;
|
3 |
+
margin-bottom: 0.9375rem
|
4 |
+
}
|
5 |
+
|
6 |
+
.icon {
|
7 |
+
position: absolute;
|
8 |
+
top: .625rem;
|
9 |
+
left: .625rem;
|
10 |
+
font-size: 1.125rem;
|
11 |
+
line-height: 1em;
|
12 |
+
}
|
13 |
+
|
14 |
+
.input {
|
15 |
+
|
16 |
+
display: block;
|
17 |
+
width: 100%;
|
18 |
+
//padding: 0.4375rem 0.75rem;
|
19 |
+
//font-size: 0.9375rem;
|
20 |
+
font-weight: 400;
|
21 |
+
line-height: 1.5;
|
22 |
+
color: var(--input-text-color);
|
23 |
+
background-color: var(--input-bg-color);
|
24 |
+
background-clip: padding-box;
|
25 |
+
border: 0.0625rem solid var(--input-border-color);
|
26 |
+
-webkit-appearance: none;
|
27 |
+
appearance: none;
|
28 |
+
border-radius: 0.75rem;
|
29 |
+
box-shadow: inset 0 0.0625rem 0.125rem rgba(0, 0, 0, .075);
|
30 |
+
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
31 |
+
|
32 |
+
padding-top: 0;
|
33 |
+
padding-right: .3125rem;
|
34 |
+
//padding-left: 0.4375rem;
|
35 |
+
padding-left: 2.5rem;
|
36 |
+
padding-bottom: 0;
|
37 |
+
font-weight: 700;
|
38 |
+
height: 2.5rem;
|
39 |
+
font-size: 1rem;
|
40 |
+
|
41 |
+
&::placeholder {
|
42 |
+
color: var(--input-placeholder-color);
|
43 |
+
font-weight: 400;
|
44 |
+
}
|
45 |
+
}
|
src/components/layout/index.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type {ComponentChildren} from "preact";
|
2 |
+
import style from "./style.module.scss"
|
3 |
+
import {Settings} from "preact-feather"
|
4 |
+
import {Route, routes} from "../../utils/route";
|
5 |
+
|
6 |
+
export function Layout(props: {
|
7 |
+
breadcrumbs: string,
|
8 |
+
title: string,
|
9 |
+
setRoute: (route: Route) => void,
|
10 |
+
children: ComponentChildren
|
11 |
+
}) {
|
12 |
+
return(
|
13 |
+
<div>
|
14 |
+
<nav className={style.breadcrumbs}>
|
15 |
+
{props.breadcrumbs}
|
16 |
+
<div className={style.actions}>
|
17 |
+
<a href="#" title="Paramètres" onClick={e => {
|
18 |
+
e.preventDefault();
|
19 |
+
props.setRoute(routes.settings);
|
20 |
+
}}>
|
21 |
+
<Settings size={18}/>
|
22 |
+
</a>
|
23 |
+
</div>
|
24 |
+
</nav>
|
25 |
+
<h2>{props.title}</h2>
|
26 |
+
{props.children}
|
27 |
+
</div>
|
28 |
+
)
|
29 |
+
}
|
src/components/layout/style.module.scss
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.breadcrumbs {
|
2 |
+
font-weight: 700;
|
3 |
+
color: var(--text-muted-color);
|
4 |
+
font-size: .8125rem;
|
5 |
+
margin-top: 1.25rem;
|
6 |
+
margin-bottom: 0.9375rem;
|
7 |
+
|
8 |
+
display: flex;
|
9 |
+
}
|
10 |
+
|
11 |
+
.actions {
|
12 |
+
margin-left: auto;
|
13 |
+
|
14 |
+
a:hover {
|
15 |
+
color: var(--text-muted-hover-color);
|
16 |
+
}
|
17 |
+
}
|
18 |
+
|
19 |
+
//.title {
|
20 |
+
// font-size: 1.59375rem;
|
21 |
+
// line-height: 1.25;
|
22 |
+
// font-weight: 500;
|
23 |
+
// color: var(--text-color);
|
24 |
+
// padding-bottom: 1.5rem;
|
25 |
+
//}
|
src/components/preview/index.tsx
ADDED
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import style from "./style.module.scss";
|
2 |
+
import {useMemo} from "preact/hooks";
|
3 |
+
import {smileysMap} from "../../utils/smileys";
|
4 |
+
|
5 |
+
// const blocks = {
|
6 |
+
// text: "text",
|
7 |
+
// img: "img",
|
8 |
+
// }
|
9 |
+
//
|
10 |
+
// type Segment = {
|
11 |
+
// type: keyof typeof blocks,
|
12 |
+
// content: string,
|
13 |
+
// }
|
14 |
+
|
15 |
+
|
16 |
+
export function Preview(props: {
|
17 |
+
raw: string,
|
18 |
+
}) {
|
19 |
+
// console.log(parseRaw(props.raw))
|
20 |
+
|
21 |
+
// const segments = useMemo(() => {
|
22 |
+
// return parseRaw(props.raw);
|
23 |
+
// }, [props.raw]);
|
24 |
+
|
25 |
+
const html = useMemo(() => {
|
26 |
+
// console.log(escapeHtml(props.raw))
|
27 |
+
const escaped = escapeHtml(props.raw);
|
28 |
+
// const withBr = escaped.replace(/\n/g, '<br/>')
|
29 |
+
// console.log(withBr)
|
30 |
+
return injectHTML(escaped).replace(/\n/g, '<br/>')
|
31 |
+
}, [props.raw]);
|
32 |
+
|
33 |
+
return (
|
34 |
+
<div className={style.wrapper} dangerouslySetInnerHTML={{__html: html}}/>
|
35 |
+
)
|
36 |
+
//
|
37 |
+
// return (
|
38 |
+
// <div className={style.wrapper}>
|
39 |
+
// {segments.map(seg => {
|
40 |
+
// if (seg.type === blocks.text) {
|
41 |
+
// return seg.content
|
42 |
+
// }
|
43 |
+
//
|
44 |
+
// if (seg.type === blocks.img) {
|
45 |
+
// return <img width="68" height="51" alt="noelshak" src={seg.content}/>
|
46 |
+
// }
|
47 |
+
// })}
|
48 |
+
// </div>
|
49 |
+
// )
|
50 |
+
}
|
51 |
+
|
52 |
+
const jvcodeMap: [RegExp, string | ((reg: RegExp, raw: string) => string)][] = [
|
53 |
+
// [/(https?:\/\/image\.noelshack\.com\/\S+)/g]: "<img width=\"68\" height=\"51\" alt=\"noelshak\" src=\"$1\"/>"
|
54 |
+
[ // Stickers
|
55 |
+
/(^| )https?:\/\/image\.noelshack\.com\/(?:fichiers|minis)(\S+)/gm,
|
56 |
+
"$1<img width=\"68\" height=\"51\" alt=\"noelshak\" src=\"https://image.noelshack.com/minis/$2\"/>"
|
57 |
+
],
|
58 |
+
[ // Vocaroo
|
59 |
+
/(^| )https:\/\/vocaroo.com\/(.+)/gm,
|
60 |
+
"$1<div><iframe width=\"300\" height=\"60\" src=\"https://vocaroo.com/embed/$2?autoplay=0\" frameborder=\"0\" allow=\"autoplay\"></iframe></div>"
|
61 |
+
],
|
62 |
+
[ // Citations
|
63 |
+
/^(?:>.*(?:\n>.*)*)/g,
|
64 |
+
(reg, raw) => {
|
65 |
+
// return raw
|
66 |
+
const match = reg.exec(raw);
|
67 |
+
if (!match) return raw;
|
68 |
+
|
69 |
+
console.log(match);
|
70 |
+
const index = match.index;
|
71 |
+
const length = match[0].length;
|
72 |
+
|
73 |
+
return raw.substring(0, index) + `<blockquote>${match[0].replace(/^>/gm, "")}</blockquote>` + raw.substring(index + length);
|
74 |
+
// console.log(match)
|
75 |
+
// const content = match[0].replace(/^>/gm, "");
|
76 |
+
// return `<blockquote>${content}</blockquote>`;
|
77 |
+
}
|
78 |
+
],
|
79 |
+
[ // Spoil
|
80 |
+
/<spoil>(.*?)<\/spoil>/gm,
|
81 |
+
(reg, raw) => { // Citations
|
82 |
+
return raw.replace(reg, (_, matched) => {
|
83 |
+
const randomId = (Math.random() + 1).toString(36).substring(2);
|
84 |
+
return `<span class="bloc-spoil-jv"><input type="checkbox" id="${randomId}" class="open-spoil"><label class="barre-head" for="${randomId}"><span class="txt-spoil">Spoil</span></label><span class="contenu-spoil">${matched}</span></span>`;
|
85 |
+
});
|
86 |
+
}
|
87 |
+
],
|
88 |
+
[ // Regular links
|
89 |
+
/(^| )(https?:\/\/\S+)/gm,
|
90 |
+
"$1<a href=\"$2\" target=\"_blank\">$2</a>"
|
91 |
+
],
|
92 |
+
|
93 |
+
// Generate regexes for smileys
|
94 |
+
// ...smileysMap.map((maping) => {
|
95 |
+
// return [new RegExp(
|
96 |
+
// Object.keys(smileysMap).map(s => (
|
97 |
+
// "(?:(?:^| )" + s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ")"
|
98 |
+
// )).join("|"), "gm"
|
99 |
+
// ), `<img src="${maping[1]}" width="16" height="16" alt=""/>`]
|
100 |
+
// })
|
101 |
+
// (() => {
|
102 |
+
// new RegExp(
|
103 |
+
// smileysMap.map((mapping) => (
|
104 |
+
// "(?:(?:^| )" + s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ")"
|
105 |
+
// )).join("|"), "gm"
|
106 |
+
// )
|
107 |
+
// return [/(https?:\/\/image\.noelshack\.com\/\S+)/g, "<img width=\"68\" height=\"51\" alt=\"noelshak\" src=\"$1\"/>"]
|
108 |
+
// })()
|
109 |
+
...smileysMap.map(mapping => {
|
110 |
+
return [
|
111 |
+
new RegExp("(?:(^| )" + mapping[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ")", "gm"),
|
112 |
+
`$1<img src="https://image.jeuxvideo.com/smileys_img/${mapping[1]}.gif" alt="${mapping[0]}"/>`
|
113 |
+
];
|
114 |
+
})
|
115 |
+
];
|
116 |
+
|
117 |
+
// console.log(jvcodeMap)
|
118 |
+
|
119 |
+
function injectHTML(input: string): string {
|
120 |
+
let previousInput;
|
121 |
+
// console.log(Object.entries(jvcodeMap))
|
122 |
+
do {
|
123 |
+
previousInput = input; // Keep track of input before replacements
|
124 |
+
for (const [regex, htmlOrFunc] of jvcodeMap) {
|
125 |
+
// const regex = new RegExp(bbCode, 'gi');
|
126 |
+
// console.log(regex, html, input)
|
127 |
+
if (htmlOrFunc instanceof Function) {
|
128 |
+
input = htmlOrFunc(regex, input);
|
129 |
+
} else {
|
130 |
+
input = input.replace(regex, htmlOrFunc as string);
|
131 |
+
}
|
132 |
+
}
|
133 |
+
// } while (input !== previousInput); // Repeat until no more replacements
|
134 |
+
} while (false); // Repeat until no more replacements
|
135 |
+
|
136 |
+
// console.log(input)
|
137 |
+
return input;
|
138 |
+
}
|
139 |
+
|
140 |
+
function escapeHtml(unsafe: string): string {
|
141 |
+
// unsafe.re
|
142 |
+
return unsafe
|
143 |
+
.replace(/&/g, "&")
|
144 |
+
.replace(/</g, "<")
|
145 |
+
.replace(/>/g, ">")
|
146 |
+
.replace(/"/g, """)
|
147 |
+
.replace(/'/g, "'");
|
148 |
+
}
|
149 |
+
|
150 |
+
// function parseRaw(raw: string): Segment[] {
|
151 |
+
// // Regex pour capturer les URLs (HTTP, HTTPS)
|
152 |
+
// const urlRegex = /(https?:\/\/[^\s]+)/g;
|
153 |
+
//
|
154 |
+
// // Découper le texte en utilisant la regex, en gardant les URLs comme blocs séparés
|
155 |
+
// return raw.split(urlRegex).filter(seg => seg !== "").map(seg => {
|
156 |
+
// return {
|
157 |
+
// type: seg.includes(".png") || seg.includes(".jpg") ? blocks.img : blocks.text,
|
158 |
+
// content: seg,
|
159 |
+
// }
|
160 |
+
// });
|
161 |
+
// }
|
src/components/preview/style.module.scss
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.wrapper {
|
2 |
+
//white-space: pre-line;
|
3 |
+
|
4 |
+
:global {
|
5 |
+
.bloc-spoil-jv {
|
6 |
+
//margin-bottom: 0.9375rem;
|
7 |
+
margin-bottom: 0;
|
8 |
+
display: inline;
|
9 |
+
|
10 |
+
.open-spoil {
|
11 |
+
position: absolute;
|
12 |
+
left: -999rem;
|
13 |
+
}
|
14 |
+
|
15 |
+
.barre-head {
|
16 |
+
//height: 1.5rem;
|
17 |
+
//line-height: 1.5rem;
|
18 |
+
//cursor: pointer;
|
19 |
+
//position: relative;
|
20 |
+
//display: block;
|
21 |
+
|
22 |
+
height: auto;
|
23 |
+
line-height: inherit;
|
24 |
+
text-align: center;
|
25 |
+
cursor: pointer;
|
26 |
+
position: relative;
|
27 |
+
display: inline;
|
28 |
+
|
29 |
+
.txt-spoil {
|
30 |
+
//position: absolute;
|
31 |
+
//top: 0;
|
32 |
+
//left: 0;
|
33 |
+
//width: 4.6875rem;
|
34 |
+
//font-size: .9285em;
|
35 |
+
//text-align: center;
|
36 |
+
//color: #fff;
|
37 |
+
//text-transform: uppercase;
|
38 |
+
//background: #fd374e;
|
39 |
+
//font-weight: 700;
|
40 |
+
//display: block;
|
41 |
+
//border-radius: 0.25rem;
|
42 |
+
|
43 |
+
position: static;
|
44 |
+
display: inline;
|
45 |
+
padding: 0.125rem 1.25rem;
|
46 |
+
line-height: inherit;
|
47 |
+
top: 0;
|
48 |
+
left: 0;
|
49 |
+
width: 4.6875rem;
|
50 |
+
font-size: .9285em;
|
51 |
+
text-align: center;
|
52 |
+
color: #fff;
|
53 |
+
text-transform: uppercase;
|
54 |
+
background: #fd374e;
|
55 |
+
font-weight: 700;
|
56 |
+
//display: block;
|
57 |
+
border-radius: 0.25rem;
|
58 |
+
}
|
59 |
+
}
|
60 |
+
|
61 |
+
.contenu-spoil {
|
62 |
+
background: #f4d6da;
|
63 |
+
color: #212121;
|
64 |
+
padding: 0.625rem;
|
65 |
+
display: none;
|
66 |
+
//text-align: justify;
|
67 |
+
text-align: left;
|
68 |
+
overflow: hidden;
|
69 |
+
}
|
70 |
+
|
71 |
+
.open-spoil:checked ~ .contenu-spoil {
|
72 |
+
display: inline;
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
|
77 |
+
blockquote {
|
78 |
+
text-align: left;
|
79 |
+
//margin-bottom: 0.9375rem;
|
80 |
+
border-left: 0.3125rem solid rgba(0, 0, 0, .2);
|
81 |
+
color: var(--text-blockquote-color);
|
82 |
+
//margin-left: 0.9375rem;
|
83 |
+
padding: 0.625rem;
|
84 |
+
background: none;
|
85 |
+
font-style: normal;
|
86 |
+
font-size: inherit;
|
87 |
+
margin: 0 0 0.9375rem 1rem;
|
88 |
+
}
|
89 |
+
|
90 |
+
a {
|
91 |
+
color: var(--link-color);
|
92 |
+
text-decoration: none;
|
93 |
+
|
94 |
+
&:hover {
|
95 |
+
color: var(--text-hover-secondary);
|
96 |
+
text-decoration: none;
|
97 |
+
}
|
98 |
+
}
|
99 |
+
}
|
100 |
+
|
101 |
+
.img {
|
102 |
+
|
103 |
+
}
|
src/components/settings/index.tsx
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Input} from "../input";
|
2 |
+
import {Link, Thermometer, Sliders} from "preact-feather";
|
3 |
+
import {Button} from "../button";
|
4 |
+
import {useEffect, useState} from "preact/hooks";
|
5 |
+
import {fetchSettings, saveSettings, Settings as SettingsType} from "../../utils/settings";
|
6 |
+
import {Slider} from "../slider";
|
7 |
+
import {FormGroup} from "../form";
|
8 |
+
|
9 |
+
export function Settings(props: {
|
10 |
+
settings: SettingsType,
|
11 |
+
setSettings: (settings: SettingsType) => void,
|
12 |
+
}) {
|
13 |
+
// const [settings, setSettings] = useState(fetchSettings)
|
14 |
+
//
|
15 |
+
// useEffect(() => {
|
16 |
+
// saveSettings(settings);
|
17 |
+
// }, [settings]);
|
18 |
+
|
19 |
+
return <div>
|
20 |
+
<form>
|
21 |
+
<FormGroup>
|
22 |
+
<label htmlFor="api">API</label>
|
23 |
+
<Input
|
24 |
+
type="text"
|
25 |
+
placeholder="URl d'API ex: https://ouruq7zepnehg2-5000.proxy.runpod.net/"
|
26 |
+
icon={Link}
|
27 |
+
value={props.settings.apiURL}
|
28 |
+
onChange={(v) => props.setSettings({...props.settings, apiURL: v as string})}
|
29 |
+
/>
|
30 |
+
</FormGroup>
|
31 |
+
<FormGroup>
|
32 |
+
<label for="temperature">Temperature</label>
|
33 |
+
<Slider
|
34 |
+
name="temperature"
|
35 |
+
value={props.settings.temperature}
|
36 |
+
onChange={(v) => props.setSettings({...props.settings, temperature: v})}
|
37 |
+
min={0}
|
38 |
+
max={5}
|
39 |
+
step={0.1}
|
40 |
+
/>
|
41 |
+
</FormGroup>
|
42 |
+
<Button
|
43 |
+
onClick={() => {
|
44 |
+
history.go(-1)
|
45 |
+
}}
|
46 |
+
>
|
47 |
+
Retour
|
48 |
+
</Button>
|
49 |
+
</form>
|
50 |
+
</div>
|
51 |
+
}
|
src/components/settings/style.module.scss
ADDED
File without changes
|
src/components/slider/index.tsx
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// import * as RadixSlider from "@radix-ui/react-slider";
|
2 |
+
// import style from "./style.module.scss";
|
3 |
+
// import {useState} from "preact/hooks";
|
4 |
+
//
|
5 |
+
// export function Slider(props: {
|
6 |
+
// value: number,
|
7 |
+
// min: number,
|
8 |
+
// max: number,
|
9 |
+
// step: number,
|
10 |
+
// onChange: (value: number) => void,
|
11 |
+
// className?: string,
|
12 |
+
// disabled?: boolean,
|
13 |
+
// }) {
|
14 |
+
// const [value, setValue] = useState(1)
|
15 |
+
//
|
16 |
+
// return (
|
17 |
+
// <div className={style.rangeSlider} style={`--min:0; --max:5; --step:0.5; --value:${value}; --text-value:"${value}"`}>
|
18 |
+
// <input
|
19 |
+
// type="range"
|
20 |
+
// min="0"
|
21 |
+
// max="5"
|
22 |
+
// step="0.1"
|
23 |
+
// value={value}
|
24 |
+
// onInput={(e) => setValue(Number((e.target as HTMLInputElement).value))}
|
25 |
+
// // onInput="this.parentNode.style.setProperty('--value',this.value); this.parentNode.style.setProperty('--text-value', JSON.stringify(this.value))"
|
26 |
+
// />
|
27 |
+
// <output></output>
|
28 |
+
// <div className={style.progress}></div>
|
29 |
+
// </div>
|
30 |
+
// )
|
31 |
+
// }
|
32 |
+
|
33 |
+
import style from "./style.module.scss";
|
34 |
+
|
35 |
+
export function Slider(props: {
|
36 |
+
name: string,
|
37 |
+
value: number,
|
38 |
+
min: number,
|
39 |
+
max: number,
|
40 |
+
step: number,
|
41 |
+
onChange: (value: number) => void,
|
42 |
+
className?: string,
|
43 |
+
disabled?: boolean,
|
44 |
+
}) {
|
45 |
+
return (
|
46 |
+
<div className={style.rangeSlider} style={`--min:${props.min}; --max:${props.max}; --step:${Math.max(props.step, 0.5)}; --value:${props.value}; --text-value:"${props.value}";`}>
|
47 |
+
<input
|
48 |
+
name={props.name}
|
49 |
+
type="range"
|
50 |
+
min={props.min}
|
51 |
+
max={props.max}
|
52 |
+
step={props.step}
|
53 |
+
value={props.value}
|
54 |
+
onInput={(e) => props.onChange(Number((e.target as HTMLInputElement).value))}
|
55 |
+
/>
|
56 |
+
<output></output>
|
57 |
+
<div className={style.progress}></div>
|
58 |
+
</div>
|
59 |
+
)
|
60 |
+
}
|
src/components/slider/style.module.scss
ADDED
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.rangeSlider {
|
2 |
+
//width: Clamp(300px, 50vw, 800px);
|
3 |
+
//min-width: 200px;
|
4 |
+
|
5 |
+
--primary-color: var(--text-secondary);
|
6 |
+
|
7 |
+
--value-offset-y: var(--ticks-gap);
|
8 |
+
--value-active-color: white;
|
9 |
+
--value-background: transparent;
|
10 |
+
--value-background-hover: var(--primary-color);
|
11 |
+
--value-font: 700 12px/1 Arial;
|
12 |
+
|
13 |
+
--fill-color: var(--primary-color);
|
14 |
+
--progress-background: #eee;
|
15 |
+
--progress-radius: 20px;
|
16 |
+
--track-height: calc(var(--thumb-size) / 2);
|
17 |
+
|
18 |
+
--min-max-font: 12px Arial;
|
19 |
+
--min-max-opacity: 0.5;
|
20 |
+
--min-max-x-offset: 50%;
|
21 |
+
|
22 |
+
--thumb-size: 20px;
|
23 |
+
--thumb-color: white;
|
24 |
+
--thumb-shadow: 0 0 3px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.5) inset,
|
25 |
+
0 0 0 99px var(--thumb-color) inset;
|
26 |
+
|
27 |
+
--thumb-shadow-active: 0 0 0 calc(var(--thumb-size) / 4) inset var(--thumb-color),
|
28 |
+
0 0 0 99px var(--primary-color) inset, 0 0 3px rgba(0, 0, 0, 0.4);
|
29 |
+
|
30 |
+
--thumb-shadow-hover: var(--thumb-shadow);
|
31 |
+
|
32 |
+
--ticks-thickness: 1px;
|
33 |
+
--ticks-height: 5px;
|
34 |
+
--ticks-gap: var(
|
35 |
+
--ticks-height,
|
36 |
+
0
|
37 |
+
); // vertical space between the ticks and the progress bar
|
38 |
+
--ticks-color: silver;
|
39 |
+
|
40 |
+
// ⚠️ BELOW VARIABLES SHOULD NOT BE CHANGED
|
41 |
+
--step: 1;
|
42 |
+
--ticks-count: Calc(var(--max) - var(--min)) / var(--step);
|
43 |
+
--maxTicksAllowed: 30;
|
44 |
+
--too-many-ticks: Min(1, Max(var(--ticks-count) - var(--maxTicksAllowed), 0));
|
45 |
+
--x-step: Max(
|
46 |
+
var(--step),
|
47 |
+
var(--too-many-ticks) * (var(--max) - var(--min))
|
48 |
+
); // manipulate the number of steps if too many ticks exist, so there would only be 2
|
49 |
+
--tickInterval: 100/ ((var(--max) - var(--min)) / var(--step)) * var(--tickEvery, 1);
|
50 |
+
--tickIntervalPerc: calc(
|
51 |
+
(100% - var(--thumb-size)) / ((var(--max) - var(--min)) / var(--x-step)) *
|
52 |
+
var(--tickEvery, 1)
|
53 |
+
);
|
54 |
+
|
55 |
+
--value-a: Clamp(
|
56 |
+
var(--min),
|
57 |
+
var(--value, 0),
|
58 |
+
var(--max)
|
59 |
+
); // default value ("--value" is used in single-range markup)
|
60 |
+
--value-b: var(--value, 0); // default value
|
61 |
+
--text-value-a: var(--text-value, "");
|
62 |
+
|
63 |
+
--completed-a: calc(
|
64 |
+
(var(--value-a) - var(--min)) / (var(--max) - var(--min)) * 100
|
65 |
+
);
|
66 |
+
--completed-b: calc(
|
67 |
+
(var(--value-b) - var(--min)) / (var(--max) - var(--min)) * 100
|
68 |
+
);
|
69 |
+
--ca: Min(var(--completed-a), var(--completed-b));
|
70 |
+
--cb: Max(var(--completed-a), var(--completed-b));
|
71 |
+
|
72 |
+
// breakdown of the below super-complex brain-breaking CSS math:
|
73 |
+
// "clamp" is used to ensure either "-1" or "1"
|
74 |
+
// "calc" is used to inflat the outcome into a huge number, to get rid of any value between -1 & 1
|
75 |
+
// if absolute diff of both completed % is above "5" (%)
|
76 |
+
// ".001" bumps the value just a bit, to avoid a scenario where calc resulted in "0" (then clamp will also be "0")
|
77 |
+
--thumbs-too-close: Clamp(
|
78 |
+
-1,
|
79 |
+
1000 * (Min(1, Max(var(--cb) - var(--ca) - 5, -1)) + 0.001),
|
80 |
+
1
|
81 |
+
);
|
82 |
+
|
83 |
+
@mixin thumb {
|
84 |
+
appearance: none;
|
85 |
+
height: var(--thumb-size);
|
86 |
+
width: var(--thumb-size);
|
87 |
+
//transform: var(--thumb-transform);
|
88 |
+
transform: translateY(-4px);
|
89 |
+
border-radius: var(--thumb-radius, 50%);
|
90 |
+
background: var(--thumb-color);
|
91 |
+
box-shadow: var(--thumb-shadow);
|
92 |
+
border: none;
|
93 |
+
pointer-events: auto;
|
94 |
+
transition: 0.1s;
|
95 |
+
}
|
96 |
+
|
97 |
+
display: inline-block;
|
98 |
+
height: Max(var(--track-height), var(--thumb-size));
|
99 |
+
// margin: calc((var(--thumb-size) - var(--track-height)) * -.25) var(--thumb-size) 0;
|
100 |
+
background: linear-gradient(
|
101 |
+
to right,
|
102 |
+
var(--ticks-color) var(--ticks-thickness),
|
103 |
+
transparent 1px
|
104 |
+
) repeat-x;
|
105 |
+
background-size: var(--tickIntervalPerc) var(--ticks-height);
|
106 |
+
background-position-x: calc(
|
107 |
+
var(--thumb-size) / 2 - var(--ticks-thickness) / 2
|
108 |
+
);
|
109 |
+
background-position-y: var(--flip-y, bottom);
|
110 |
+
|
111 |
+
padding-bottom: var(--flip-y, var(--ticks-gap));
|
112 |
+
padding-top: calc(var(--flip-y) * var(--ticks-gap));
|
113 |
+
margin-top: 1rem;
|
114 |
+
margin-bottom: 2rem;
|
115 |
+
|
116 |
+
position: relative;
|
117 |
+
z-index: 1;
|
118 |
+
|
119 |
+
&[data-ticks-position="top"] {
|
120 |
+
--flip-y: 1;
|
121 |
+
}
|
122 |
+
|
123 |
+
// mix/max texts
|
124 |
+
&::before,
|
125 |
+
&::after {
|
126 |
+
--offset: calc(var(--thumb-size) / 2);
|
127 |
+
content: counter(x);
|
128 |
+
display: var(--show-min-max, block);
|
129 |
+
font: var(--min-max-font);
|
130 |
+
position: absolute;
|
131 |
+
bottom: var(--flip-y, -2.5ch);
|
132 |
+
top: calc(-2.5ch * var(--flip-y));
|
133 |
+
opacity: var(--min-max-opacity);
|
134 |
+
//transform: translateX(calc(var(--min-max-x-offset) * var(--before, -1) * -1)) scale(var(--at-edge));
|
135 |
+
transform: translateX(calc(var(--min-max-x-offset) * var(--before, -1) * -1));
|
136 |
+
pointer-events: none;
|
137 |
+
}
|
138 |
+
|
139 |
+
&::before {
|
140 |
+
--before: 1;
|
141 |
+
counter-reset: x var(--min);
|
142 |
+
left: var(--offset);
|
143 |
+
}
|
144 |
+
|
145 |
+
&::after {
|
146 |
+
counter-reset: x var(--max);
|
147 |
+
right: var(--offset);
|
148 |
+
}
|
149 |
+
|
150 |
+
&__values {
|
151 |
+
position: relative;
|
152 |
+
top: 50%;
|
153 |
+
line-height: 0;
|
154 |
+
text-align: justify;
|
155 |
+
width: 100%;
|
156 |
+
pointer-events: none;
|
157 |
+
margin: 0 auto;
|
158 |
+
z-index: 5;
|
159 |
+
|
160 |
+
// trick so "justify" will work
|
161 |
+
&::after {
|
162 |
+
content: "";
|
163 |
+
width: 100%;
|
164 |
+
display: inline-block;
|
165 |
+
height: 0;
|
166 |
+
background: red;
|
167 |
+
}
|
168 |
+
}
|
169 |
+
|
170 |
+
.progress {
|
171 |
+
--start-end: calc(var(--thumb-size) / 2);
|
172 |
+
--clip-end: calc(100% - (var(--cb)) * 1%);
|
173 |
+
--clip-start: calc(var(--ca) * 1%);
|
174 |
+
--clip: inset(-20px var(--clip-end) -20px var(--clip-start));
|
175 |
+
position: absolute;
|
176 |
+
left: calc(var(--start-end) - 10px);
|
177 |
+
right: calc(var(--start-end) - 10px);
|
178 |
+
top: calc(
|
179 |
+
var(--ticks-gap) * var(--flip-y, 0) + var(--thumb-size) / 2 -
|
180 |
+
var(--track-height)
|
181 |
+
);
|
182 |
+
// transform: var(--flip-y, translateY(-50%) translateZ(0));
|
183 |
+
height: calc(var(--track-height));
|
184 |
+
background: var(--progress-background, #eee);
|
185 |
+
pointer-events: none;
|
186 |
+
z-index: -1;
|
187 |
+
border-radius: var(--progress-radius);
|
188 |
+
|
189 |
+
// fill area
|
190 |
+
&::before {
|
191 |
+
content: "";
|
192 |
+
position: absolute;
|
193 |
+
// left: Clamp(0%, calc(var(--ca) * 1%), 100%); // confine to 0 or above
|
194 |
+
// width: Min(100%, calc((var(--cb) - var(--ca)) * 1%)); // confine to maximum 100%
|
195 |
+
left: 0;
|
196 |
+
right: 0;
|
197 |
+
clip-path: var(--clip);
|
198 |
+
top: 0;
|
199 |
+
bottom: 0;
|
200 |
+
background: var(--fill-color, black);
|
201 |
+
box-shadow: var(--progress-flll-shadow);
|
202 |
+
z-index: 1;
|
203 |
+
border-radius: inherit;
|
204 |
+
}
|
205 |
+
|
206 |
+
// shadow-effect
|
207 |
+
&::after {
|
208 |
+
content: "";
|
209 |
+
position: absolute;
|
210 |
+
top: 0;
|
211 |
+
right: 0;
|
212 |
+
bottom: 0;
|
213 |
+
left: 0;
|
214 |
+
box-shadow: var(--progress-shadow);
|
215 |
+
pointer-events: none;
|
216 |
+
border-radius: inherit;
|
217 |
+
}
|
218 |
+
}
|
219 |
+
|
220 |
+
& > input {
|
221 |
+
-webkit-appearance: none;
|
222 |
+
width: 100%;
|
223 |
+
height: var(--thumb-size);
|
224 |
+
margin: 0;
|
225 |
+
position: absolute;
|
226 |
+
left: 0;
|
227 |
+
top: calc(
|
228 |
+
50% - Max(var(--track-height), var(--thumb-size)) / 2 +
|
229 |
+
calc(var(--ticks-gap) / 2 * var(--flip-y, -1))
|
230 |
+
);
|
231 |
+
cursor: -webkit-grab;
|
232 |
+
cursor: grab;
|
233 |
+
outline: none;
|
234 |
+
background: none;
|
235 |
+
|
236 |
+
&:not(:only-of-type) {
|
237 |
+
pointer-events: none;
|
238 |
+
}
|
239 |
+
|
240 |
+
&::-webkit-slider-thumb {
|
241 |
+
@include thumb;
|
242 |
+
}
|
243 |
+
|
244 |
+
&::-moz-range-thumb {
|
245 |
+
@include thumb;
|
246 |
+
}
|
247 |
+
|
248 |
+
&::-ms-thumb {
|
249 |
+
@include thumb;
|
250 |
+
}
|
251 |
+
|
252 |
+
&:hover {
|
253 |
+
--thumb-shadow: var(--thumb-shadow-hover);
|
254 |
+
|
255 |
+
& + output {
|
256 |
+
--value-background: var(--value-background-hover);
|
257 |
+
--y-offset: -8px;
|
258 |
+
color: var(--value-active-color);
|
259 |
+
box-shadow: 0 0 0 3px var(--value-background);
|
260 |
+
}
|
261 |
+
}
|
262 |
+
|
263 |
+
&:active {
|
264 |
+
--thumb-shadow: var(--thumb-shadow-active);
|
265 |
+
cursor: grabbing;
|
266 |
+
z-index: 2; // when sliding left thumb over the right or vice-versa, make sure the moved thumb is on top
|
267 |
+
+ output {
|
268 |
+
transition: 0s;
|
269 |
+
}
|
270 |
+
}
|
271 |
+
|
272 |
+
&:nth-of-type(1) {
|
273 |
+
--is-left-most: Clamp(0, (var(--value-a) - var(--value-b)) * 99999, 1);
|
274 |
+
|
275 |
+
& + output {
|
276 |
+
&:not(:only-of-type) {
|
277 |
+
--flip: calc(var(--thumbs-too-close) * -1);
|
278 |
+
}
|
279 |
+
|
280 |
+
--value: var(--value-a);
|
281 |
+
--x-offset: calc(var(--completed-a) * -1%);
|
282 |
+
|
283 |
+
&::after {
|
284 |
+
content: var(--prefix, "") var(--text-value-a) var(--suffix, "");
|
285 |
+
}
|
286 |
+
}
|
287 |
+
}
|
288 |
+
|
289 |
+
&:nth-of-type(2) {
|
290 |
+
--is-left-most: Clamp(0, (var(--value-b) - var(--value-a)) * 99999, 1);
|
291 |
+
|
292 |
+
& + output {
|
293 |
+
--value: var(--value-b);
|
294 |
+
}
|
295 |
+
}
|
296 |
+
|
297 |
+
// non-multiple range should not clip start of progress bar
|
298 |
+
&:only-of-type {
|
299 |
+
~ .progress {
|
300 |
+
--clip-start: 0;
|
301 |
+
}
|
302 |
+
}
|
303 |
+
|
304 |
+
& + output {
|
305 |
+
--flip: -1;
|
306 |
+
--x-offset: calc(var(--completed-b) * -1%);
|
307 |
+
--pos: calc(
|
308 |
+
((var(--value) - var(--min)) / (var(--max) - var(--min))) * 100%
|
309 |
+
);
|
310 |
+
|
311 |
+
pointer-events: none;
|
312 |
+
position: absolute;
|
313 |
+
z-index: 5;
|
314 |
+
background: var(--value-background);
|
315 |
+
border-radius: 10px;
|
316 |
+
//padding: 2px 4px;
|
317 |
+
padding: 0 4px;
|
318 |
+
left: var(--pos);
|
319 |
+
transform: translate(var(--x-offset), calc(150% * var(--flip) - (var(--y-offset, 0px) + var(--value-offset-y)) * var(--flip)));
|
320 |
+
transition: all 0.12s ease-out, left 0s;
|
321 |
+
|
322 |
+
&::after {
|
323 |
+
content: var(--prefix, "") var(--text-value-b) var(--suffix, "");
|
324 |
+
font: var(--value-font);
|
325 |
+
}
|
326 |
+
}
|
327 |
+
}
|
328 |
+
}
|
329 |
+
|
330 |
+
|
src/components/spinner/index.tsx
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cn from "classnames"
|
2 |
+
import style from "./style.module.scss"
|
3 |
+
|
4 |
+
export function Spinner(props: {className?: string}) {
|
5 |
+
return (
|
6 |
+
<div className={cn(style.spinnerSquare, props.className)}>
|
7 |
+
<div className={style.square1}></div>
|
8 |
+
<div className={style.square2}></div>
|
9 |
+
<div className={style.square3}></div>
|
10 |
+
</div>
|
11 |
+
)
|
12 |
+
}
|
src/components/spinner/style.module.scss
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.spinnerSquare {
|
2 |
+
display: flex;
|
3 |
+
flex-direction: row;
|
4 |
+
width: 90px;
|
5 |
+
height: 120px;
|
6 |
+
|
7 |
+
> div {
|
8 |
+
width: 17px;
|
9 |
+
height: 80px;
|
10 |
+
margin: auto auto;
|
11 |
+
border-radius: 4px;
|
12 |
+
}
|
13 |
+
}
|
14 |
+
|
15 |
+
.square1 {
|
16 |
+
animation: square-anim 1200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) 0s infinite;
|
17 |
+
}
|
18 |
+
|
19 |
+
.square2 {
|
20 |
+
animation: square-anim 1200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) 200ms infinite;
|
21 |
+
}
|
22 |
+
|
23 |
+
.square3 {
|
24 |
+
animation: square-anim 1200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) 400ms infinite;
|
25 |
+
}
|
26 |
+
|
27 |
+
@keyframes square-anim {
|
28 |
+
0% {
|
29 |
+
height: 80px;
|
30 |
+
background-color: var(--text-secondary);
|
31 |
+
}
|
32 |
+
20% {
|
33 |
+
height: 80px;
|
34 |
+
}
|
35 |
+
40% {
|
36 |
+
height: 120px;
|
37 |
+
background-color: var(--text-hover-secondary);
|
38 |
+
}
|
39 |
+
80% {
|
40 |
+
height: 80px;
|
41 |
+
}
|
42 |
+
100% {
|
43 |
+
height: 80px;
|
44 |
+
background-color: var(--text-secondary);
|
45 |
+
}
|
46 |
+
}
|
src/components/topic/index.tsx
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Post as PostType, Topic as TopicType} from "../../utils/topics";
|
2 |
+
import style from "./style.module.scss";
|
3 |
+
import {Preview} from "../preview";
|
4 |
+
import {iso8601ToFrench} from "../../utils/dates";
|
5 |
+
import {FormGroup} from "../form";
|
6 |
+
import {Slider} from "../slider";
|
7 |
+
import {Button} from "../button";
|
8 |
+
import {Settings as SettingsType, Settings} from "../../utils/settings";
|
9 |
+
|
10 |
+
export function Topic(props: {
|
11 |
+
topic: TopicType,
|
12 |
+
settings: Settings,
|
13 |
+
setSettings: (settings: SettingsType) => void,
|
14 |
+
addPosts: (topicId: string, postsCount: number) => Promise<void>,
|
15 |
+
pendingGeneration: boolean,
|
16 |
+
}) {
|
17 |
+
console.log(props.topic)
|
18 |
+
|
19 |
+
return (
|
20 |
+
<div>
|
21 |
+
{props.topic.posts.map(post => <Post post={post}/>)}
|
22 |
+
<div>
|
23 |
+
<h2>Ajout de posts</h2>
|
24 |
+
<div className={style.generationSettings}>
|
25 |
+
<FormGroup>
|
26 |
+
<label htmlFor="postCount">Nombre de posts</label>
|
27 |
+
<Slider
|
28 |
+
name="postCount"
|
29 |
+
value={props.settings.postCount}
|
30 |
+
// onChange={(v) => props.setSettings({...props.settings, temperature: v as number})}
|
31 |
+
// onChange={setGenerationPostCount}
|
32 |
+
onChange={(v) => props.setSettings({...props.settings, postCount: v})}
|
33 |
+
min={1}
|
34 |
+
max={10}
|
35 |
+
step={1}
|
36 |
+
/>
|
37 |
+
</FormGroup>
|
38 |
+
</div>
|
39 |
+
<Button
|
40 |
+
onClick={() => props.addPosts(props.topic.id, props.settings.postCount)}
|
41 |
+
secondary={true}
|
42 |
+
loading={props.pendingGeneration}
|
43 |
+
>
|
44 |
+
Générer
|
45 |
+
</Button>
|
46 |
+
</div>
|
47 |
+
<hr/>
|
48 |
+
</div>
|
49 |
+
)
|
50 |
+
}
|
51 |
+
|
52 |
+
function Post(props: {
|
53 |
+
post: PostType
|
54 |
+
}) {
|
55 |
+
return (
|
56 |
+
<div className={style.post}>
|
57 |
+
<div className={style.postHeader}>
|
58 |
+
<img src="https://image.jeuxvideo.com/avatar-sm/default.jpg" className={style.avatar} alt="ahi"/>
|
59 |
+
<div className={style.user}>{props.post.user}</div>
|
60 |
+
<div className={style.date}>{iso8601ToFrench(props.post.date)}</div>
|
61 |
+
</div>
|
62 |
+
<Preview raw={props.post.content}/>
|
63 |
+
</div>
|
64 |
+
)
|
65 |
+
}
|
src/components/topic/style.module.scss
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.post {
|
2 |
+
margin-bottom: 0.9375rem;
|
3 |
+
font-size: .9375rem;
|
4 |
+
line-height: 1.5;
|
5 |
+
background: var(--block-bg-color);
|
6 |
+
border: 0.0625rem solid var(--border-color);
|
7 |
+
border-radius: 0.5rem;
|
8 |
+
overflow: hidden;
|
9 |
+
padding: 0.75rem;
|
10 |
+
}
|
11 |
+
|
12 |
+
.postHeader {
|
13 |
+
border-bottom: 0.0625rem dotted var(--border-color);
|
14 |
+
height: 3.625rem;
|
15 |
+
margin-bottom: 0.75rem;
|
16 |
+
display: grid;
|
17 |
+
grid-template-areas:
|
18 |
+
"avatar user"
|
19 |
+
"avatar date";
|
20 |
+
grid-template-rows: 1.5rem 1.5rem;
|
21 |
+
grid-template-columns: auto 1fr;
|
22 |
+
column-gap: .625rem;
|
23 |
+
}
|
24 |
+
|
25 |
+
.avatar {
|
26 |
+
grid-area: avatar;
|
27 |
+
height: 3rem;
|
28 |
+
width: 3rem;
|
29 |
+
object-fit: cover;
|
30 |
+
border-radius: 50%;
|
31 |
+
}
|
32 |
+
|
33 |
+
.user {
|
34 |
+
grid-area: user;
|
35 |
+
font-size: 1.0625rem;
|
36 |
+
font-weight: 500;
|
37 |
+
font-family: "Roboto", Arial, Helvetica, sans-serif;
|
38 |
+
line-height: 1.25;
|
39 |
+
}
|
40 |
+
|
41 |
+
.date {
|
42 |
+
grid-area: date;
|
43 |
+
color: var(--link-color);
|
44 |
+
text-decoration: none;
|
45 |
+
font-size: .8125rem;
|
46 |
+
}
|
47 |
+
|
48 |
+
.generationSettings {
|
49 |
+
display: grid;
|
50 |
+
grid-template-columns: 1fr 1fr;
|
51 |
+
}
|
src/components/topics/index.tsx
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Topic} from "../../utils/topics";
|
2 |
+
import {Layout} from "../layout";
|
3 |
+
import {Spinner} from "../spinner";
|
4 |
+
import style from "./style.module.scss";
|
5 |
+
import {Route, routes, RouteSetter} from "../../utils/route";
|
6 |
+
import {Button} from "../button";
|
7 |
+
import {Input} from "../input";
|
8 |
+
import {Link, Thermometer, Sliders} from "preact-feather";
|
9 |
+
import {Settings as SettingsType, Settings} from "../../utils/settings";
|
10 |
+
import {useMemo, useState} from "preact/hooks";
|
11 |
+
import {frenchToIso8601, iso8601ToFrench} from "../../utils/dates";
|
12 |
+
import {FormGroup} from "../form";
|
13 |
+
import {Slider} from "../slider";
|
14 |
+
import cn from "classnames";
|
15 |
+
|
16 |
+
export function Topics(props: {
|
17 |
+
topics: Topic[] | null,
|
18 |
+
setRoute: RouteSetter,
|
19 |
+
settings: Settings,
|
20 |
+
setSettings: (settings: SettingsType) => void,
|
21 |
+
generateTopic: (postCount: number) => Promise<void>,
|
22 |
+
pendingGeneration: boolean,
|
23 |
+
latestGeneratedTopicId: string|null,
|
24 |
+
}) {
|
25 |
+
// const [generationPostCount, setGenerationPostCount] = useState<number>(1)
|
26 |
+
|
27 |
+
const sortedTopics = useMemo(() => {
|
28 |
+
if (props.topics === null || props.topics.length < 1) {
|
29 |
+
return props.topics;
|
30 |
+
}
|
31 |
+
|
32 |
+
return props.topics.sort((topicA, topicB) => {
|
33 |
+
if (topicA.posts.length < 1 || topicB.posts.length < 1) {
|
34 |
+
return 0;
|
35 |
+
}
|
36 |
+
return topicB.posts[topicB.posts.length - 1].date.localeCompare(topicA.posts[topicA.posts.length - 1].date);
|
37 |
+
});
|
38 |
+
}, [props.topics]);
|
39 |
+
|
40 |
+
return (
|
41 |
+
<div>
|
42 |
+
{
|
43 |
+
sortedTopics === null ?
|
44 |
+
<Spinner className={style.spinner}/> :
|
45 |
+
<List topics={sortedTopics} setRoute={props.setRoute} latestGeneratedTopicId={props.latestGeneratedTopicId}/>
|
46 |
+
}
|
47 |
+
<div>
|
48 |
+
<h2>Nouveau topic</h2>
|
49 |
+
<div className={style.generationSettings}>
|
50 |
+
<FormGroup>
|
51 |
+
<label for="postCount">Nombre de posts</label>
|
52 |
+
<Slider
|
53 |
+
name="postCount"
|
54 |
+
value={props.settings.postCount}
|
55 |
+
// onChange={(v) => props.setSettings({...props.settings, temperature: v as number})}
|
56 |
+
// onChange={setGenerationPostCount}
|
57 |
+
onChange={(v) => props.setSettings({...props.settings, postCount: v})}
|
58 |
+
min={1}
|
59 |
+
max={10}
|
60 |
+
step={1}
|
61 |
+
/>
|
62 |
+
</FormGroup>
|
63 |
+
</div>
|
64 |
+
<Button
|
65 |
+
onClick={() => props.generateTopic(props.settings.postCount)}
|
66 |
+
secondary={true}
|
67 |
+
loading={props.pendingGeneration}
|
68 |
+
>
|
69 |
+
Générer
|
70 |
+
</Button>
|
71 |
+
</div>
|
72 |
+
<hr/>
|
73 |
+
</div>
|
74 |
+
)
|
75 |
+
}
|
76 |
+
|
77 |
+
function List(props: {
|
78 |
+
topics: Topic[],
|
79 |
+
setRoute: RouteSetter,
|
80 |
+
latestGeneratedTopicId: string|null,
|
81 |
+
}) {
|
82 |
+
return (
|
83 |
+
<ul className={style.list}>
|
84 |
+
<li className={style.head}>
|
85 |
+
<span>Sujet</span>
|
86 |
+
<span>Auteur</span>
|
87 |
+
<span>NB</span>
|
88 |
+
<span>Dernier msg</span>
|
89 |
+
</li>
|
90 |
+
{props.topics.map(topic => (
|
91 |
+
<li className={cn({[style.highlight]: topic.id === props.latestGeneratedTopicId})}>
|
92 |
+
<span>
|
93 |
+
<a href="#" onClick={(e) => {
|
94 |
+
e.preventDefault();
|
95 |
+
props.setRoute(routes.topic, 0, topic.id);
|
96 |
+
}}>
|
97 |
+
{topic.title}
|
98 |
+
</a>
|
99 |
+
</span>
|
100 |
+
<span>{topic.posts[0].user}</span>
|
101 |
+
<span>{topic.posts.length}</span>
|
102 |
+
<span>{iso8601ToFrench(topic.posts[topic.posts.length - 1].date)}</span>
|
103 |
+
</li>
|
104 |
+
))}
|
105 |
+
</ul>
|
106 |
+
)
|
107 |
+
}
|
src/components/topics/style.module.scss
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.spinner {
|
2 |
+
margin: 5rem auto;
|
3 |
+
}
|
4 |
+
|
5 |
+
.list {
|
6 |
+
display: grid;
|
7 |
+
grid-template-columns: 10fr 3fr 1fr 3fr;
|
8 |
+
|
9 |
+
list-style-type: none;
|
10 |
+
//margin: 0;
|
11 |
+
//margin-bottom: 1.5rem;
|
12 |
+
background: var(--block-bg-color);
|
13 |
+
border: .0625rem solid var(--border-color);
|
14 |
+
border-radius: 0.75rem;
|
15 |
+
width: 100%;
|
16 |
+
padding: 0;
|
17 |
+
overflow: hidden;
|
18 |
+
margin-bottom: 1.5rem;
|
19 |
+
|
20 |
+
> li {
|
21 |
+
display: contents;
|
22 |
+
list-style: none;
|
23 |
+
padding: 0;
|
24 |
+
margin: 0;
|
25 |
+
|
26 |
+
> span {
|
27 |
+
font-weight: var(--topic-font-weight);
|
28 |
+
font-size: var(--topic-font-size);
|
29 |
+
padding: var(--topic-gap);
|
30 |
+
|
31 |
+
a {
|
32 |
+
overflow: hidden;
|
33 |
+
display: -webkit-box;
|
34 |
+
-webkit-line-clamp: var(--topic-title-lines);
|
35 |
+
-webkit-box-orient: vertical;
|
36 |
+
//margin-left: var(--topic-gap);
|
37 |
+
color: var(--link-color);
|
38 |
+
text-decoration: none;
|
39 |
+
font-size: var(--topic-title-font-size);
|
40 |
+
font-weight: var(--topic-title-font-weight);
|
41 |
+
|
42 |
+
&:hover {
|
43 |
+
color: var(--text-hover-secondary);
|
44 |
+
}
|
45 |
+
}
|
46 |
+
}
|
47 |
+
|
48 |
+
&:nth-child(even) {
|
49 |
+
> span {
|
50 |
+
background: var(--block-even-bg-color);
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
&.highlight {
|
55 |
+
> span {
|
56 |
+
background: var(--block-highlighted-bg-color);
|
57 |
+
}
|
58 |
+
}
|
59 |
+
}
|
60 |
+
}
|
61 |
+
|
62 |
+
.head {
|
63 |
+
> span {
|
64 |
+
text-transform: uppercase;
|
65 |
+
}
|
66 |
+
}
|
67 |
+
|
68 |
+
.generationSettings {
|
69 |
+
display: grid;
|
70 |
+
grid-template-columns: 1fr 1fr;
|
71 |
+
}
|
src/index.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {render, h} from "preact";
|
2 |
+
import {App} from "./app";
|
3 |
+
|
4 |
+
render(h(App, null), document.getElementById('app'))
|
src/reset.scss
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/***
|
2 |
+
The new CSS reset - version 1.11.3 (last updated 25.08.2024)
|
3 |
+
GitHub page: https://github.com/elad2412/the-new-css-reset
|
4 |
+
***/
|
5 |
+
|
6 |
+
/*
|
7 |
+
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
8 |
+
- The "symbol *" part is to solve Firefox SVG sprite bug
|
9 |
+
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
|
10 |
+
*/
|
11 |
+
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
|
12 |
+
all: unset;
|
13 |
+
display: revert;
|
14 |
+
}
|
15 |
+
|
16 |
+
/* Preferred box-sizing value */
|
17 |
+
*,
|
18 |
+
*::before,
|
19 |
+
*::after {
|
20 |
+
box-sizing: border-box;
|
21 |
+
}
|
22 |
+
|
23 |
+
/* Fix mobile Safari increase font-size on landscape mode */
|
24 |
+
html {
|
25 |
+
-moz-text-size-adjust: none;
|
26 |
+
-webkit-text-size-adjust: none;
|
27 |
+
text-size-adjust: none;
|
28 |
+
}
|
29 |
+
|
30 |
+
/* Reapply the pointer cursor for anchor tags */
|
31 |
+
a, button {
|
32 |
+
cursor: revert;
|
33 |
+
}
|
34 |
+
|
35 |
+
/* Remove list styles (bullets/numbers) */
|
36 |
+
ol, ul, menu, summary {
|
37 |
+
list-style: none;
|
38 |
+
}
|
39 |
+
|
40 |
+
/* Firefox: solve issue where nested ordered lists continue numbering from parent (https://bugzilla.mozilla.org/show_bug.cgi?id=1881517) */
|
41 |
+
ol {
|
42 |
+
counter-reset: revert;
|
43 |
+
}
|
44 |
+
|
45 |
+
/* For images to not be able to exceed their container */
|
46 |
+
img {
|
47 |
+
max-inline-size: 100%;
|
48 |
+
max-block-size: 100%;
|
49 |
+
}
|
50 |
+
|
51 |
+
/* removes spacing between cells in tables */
|
52 |
+
table {
|
53 |
+
border-collapse: collapse;
|
54 |
+
}
|
55 |
+
|
56 |
+
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
57 |
+
input, textarea {
|
58 |
+
-webkit-user-select: auto;
|
59 |
+
}
|
60 |
+
|
61 |
+
/* revert the 'white-space' property for textarea elements on Safari */
|
62 |
+
textarea {
|
63 |
+
white-space: revert;
|
64 |
+
}
|
65 |
+
|
66 |
+
/* minimum style to allow to style meter element */
|
67 |
+
meter {
|
68 |
+
-webkit-appearance: revert;
|
69 |
+
appearance: revert;
|
70 |
+
}
|
71 |
+
|
72 |
+
/* preformatted text - use only for this feature */
|
73 |
+
:where(pre) {
|
74 |
+
all: revert;
|
75 |
+
box-sizing: border-box;
|
76 |
+
}
|
77 |
+
|
78 |
+
/* reset default text opacity of input placeholder */
|
79 |
+
::placeholder {
|
80 |
+
color: unset;
|
81 |
+
}
|
82 |
+
|
83 |
+
/* fix the feature of 'hidden' attribute.
|
84 |
+
display:revert; revert to element instead of attribute */
|
85 |
+
:where([hidden]) {
|
86 |
+
display: none;
|
87 |
+
}
|
88 |
+
|
89 |
+
/* revert for bug in Chromium browsers
|
90 |
+
- fix for the content editable attribute will work properly.
|
91 |
+
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
92 |
+
:where([contenteditable]:not([contenteditable="false"])) {
|
93 |
+
-moz-user-modify: read-write;
|
94 |
+
-webkit-user-modify: read-write;
|
95 |
+
overflow-wrap: break-word;
|
96 |
+
-webkit-line-break: after-white-space;
|
97 |
+
-webkit-user-select: auto;
|
98 |
+
}
|
99 |
+
|
100 |
+
/* apply back the draggable feature - exist only in Chromium and Safari */
|
101 |
+
:where([draggable="true"]) {
|
102 |
+
-webkit-user-drag: element;
|
103 |
+
}
|
104 |
+
|
105 |
+
/* Revert Modal native behavior */
|
106 |
+
//noinspection CssInvalidPseudoSelector
|
107 |
+
:where(dialog:modal) {
|
108 |
+
all: revert;
|
109 |
+
box-sizing: border-box;
|
110 |
+
}
|
111 |
+
|
112 |
+
/* Remove details summary webkit styles */
|
113 |
+
::-webkit-details-marker {
|
114 |
+
display: none;
|
115 |
+
}
|
src/style.module.scss
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.header {
|
2 |
+
background-color: var(--header-bg-color);
|
3 |
+
color: var(--text-color);
|
4 |
+
}
|
5 |
+
|
6 |
+
.logo {
|
7 |
+
font-size: 2rem;
|
8 |
+
font-weight: 700;
|
9 |
+
}
|
10 |
+
|
11 |
+
.main {
|
12 |
+
|
13 |
+
}
|
src/styles.scss
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@use "./reset";
|
2 |
+
|
3 |
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap');
|
4 |
+
|
5 |
+
// JVC color theme
|
6 |
+
:root {
|
7 |
+
color-scheme: dark;
|
8 |
+
--body-bg: #22252a;
|
9 |
+
--text-primary: #3d87f5;
|
10 |
+
--text-hover-primary: #5596f6;
|
11 |
+
--text-secondary: #f66031;
|
12 |
+
--text-hover-secondary: #f7734a;
|
13 |
+
--text-danger: #e4606d;
|
14 |
+
--text-hover-danger: #dc3545;
|
15 |
+
--text-dark: #e0e0e0;
|
16 |
+
--text-hover-dark: #fff;
|
17 |
+
--text-light: #000;
|
18 |
+
--bg-color-light: #272a30;
|
19 |
+
--bg-color-light-transparent: rgba(39, 42, 48, 0);
|
20 |
+
--bg-color: #22252a;
|
21 |
+
--text-color: #f2f2f2;
|
22 |
+
--border-color: #4a4c4f;
|
23 |
+
--border-even-color: #454f5e;
|
24 |
+
--border-highlighted-color: #765d3f;
|
25 |
+
--block-bg-color: #2e3238;
|
26 |
+
--block-even-bg-color: #363e49;
|
27 |
+
--block-highlighted-bg-color: #594126;
|
28 |
+
--block-bg-color-hover: #353941;
|
29 |
+
--block-bg-color-hover-dark: #454b54;
|
30 |
+
--text-muted-color: #9e9e9e;
|
31 |
+
--text-muted-hover-color: #bababa;
|
32 |
+
--text-blockquote-color: #c7c7d1;
|
33 |
+
--link-color: #7dc3f7;
|
34 |
+
--link-hover-bg-color: #4c5767;
|
35 |
+
--header-bg-color: #18191b;
|
36 |
+
--header-bottom-bg-color: #000;
|
37 |
+
--header-bottom-bg-color-hover: #303236;
|
38 |
+
--video-footer-bg-color: #1d1e20;
|
39 |
+
--layout-bg-color: #292d32;
|
40 |
+
--disabled-bg-color: #1d1e20;
|
41 |
+
--disabled-text-color: #787e87;
|
42 |
+
--disabled-border-color: #787e87;
|
43 |
+
--ad-bg-color: #18191b;
|
44 |
+
--ad-border-color: #000;
|
45 |
+
--game-header-bg-color: #2c2f35;
|
46 |
+
--bg-light: #2c2f35;
|
47 |
+
--game-tab-hover-bg-color: #17191c;
|
48 |
+
--price-color: #9fafc6;
|
49 |
+
--blue-light: #414a58;
|
50 |
+
--live-color: #ff4d4d;
|
51 |
+
--live-hover-color: #ff1a1a;
|
52 |
+
--gauge-bg-color: #616161;
|
53 |
+
--blue-gray-color: #9fafc6;
|
54 |
+
--message-delete: #104e60;
|
55 |
+
--message-delete-border: #092a34;
|
56 |
+
--message-delete-gta: #46435b;
|
57 |
+
--message-delete-gta-border: #191820;
|
58 |
+
--input-bg-color: #303236;
|
59 |
+
--input-border-color: #636569;
|
60 |
+
--input-text-color: #f2f2f2;
|
61 |
+
--input-placeholder-color: #616161;
|
62 |
+
--form-select-indicator: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath stroke=%27%23e0e0e0%27 stroke-width=%272px%27 d=%27M2 5l6 6 6-6%27 fill=%27none%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27/%3e%3c/svg%3e");
|
63 |
+
--form-switch-bg-image: url("data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27><circle r=%273%27 fill=%27%23636569%27/></svg>");
|
64 |
+
--admin-color: #f24444;
|
65 |
+
--modo-color: #42b228;
|
66 |
+
--ad-placeholder-opacity: 0.4;
|
67 |
+
--alert-brightness: 0.85;
|
68 |
+
--filters-bg-color: #16191d;
|
69 |
+
--gray-100: #f8f9fa;
|
70 |
+
--gray-200: #e9ecef;
|
71 |
+
--gray-300: #dee2e6;
|
72 |
+
--gray-400: #ced4da;
|
73 |
+
--gray-500: #adb5bd;
|
74 |
+
--gray-600: #6c757d;
|
75 |
+
--gray-700: #495057;
|
76 |
+
--gray-800: #343a40;
|
77 |
+
--gray-900: #212529;
|
78 |
+
--white-rgb: #fff;
|
79 |
+
--black-rgb: #000;
|
80 |
+
--body-color-rgb: var(--text-color);
|
81 |
+
--body-bg-rgb: var(--body-bg);
|
82 |
+
--font-sans-serif: "Roboto", Arial, Helvetica, sans-serif;
|
83 |
+
--font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
84 |
+
--gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
85 |
+
--root-font-size: 1rem;
|
86 |
+
--body-font-family: "Roboto", Arial, Helvetica, sans-serif;
|
87 |
+
--body-font-size: 0.9375rem;
|
88 |
+
--body-font-weight: 400;
|
89 |
+
--body-line-height: 1.5;
|
90 |
+
--body-color: var(--text-color);
|
91 |
+
--border-width: 0.0625rem;
|
92 |
+
--border-style: solid;
|
93 |
+
--border-color-translucent: rgba(0, 0, 0, 0.175);
|
94 |
+
--border-radius: 0.75rem;
|
95 |
+
--border-radius-sm: 0.5rem;
|
96 |
+
--border-radius-lg: 0.75rem;
|
97 |
+
--border-radius-xl: 1rem;
|
98 |
+
--border-radius-2xl: 2rem;
|
99 |
+
--border-radius-pill: 50rem;
|
100 |
+
--link-hover-color: var(--text-hover-secondary);
|
101 |
+
--code-color: #d63384;
|
102 |
+
--highlight-bg: #fff3cd;
|
103 |
+
--topic-font-size: 0.8125rem;
|
104 |
+
--topic-font-weight: 700;
|
105 |
+
--topic-line-height: 1.25;
|
106 |
+
--topic-gap: 0.625rem;
|
107 |
+
--topic-title-font-size: 0.9375rem;
|
108 |
+
--topic-title-font-weight: 500;
|
109 |
+
}
|
110 |
+
|
111 |
+
body {
|
112 |
+
margin: 0;
|
113 |
+
font-family: var(--body-font-family), sans-serif;
|
114 |
+
font-size: var(--body-font-size);
|
115 |
+
font-weight: var(--body-font-weight);
|
116 |
+
line-height: var(--body-line-height);
|
117 |
+
color: var(--body-color);
|
118 |
+
text-align: var(--body-text-align);
|
119 |
+
background-color: var(--body-bg);
|
120 |
+
-webkit-text-size-adjust: 100%;
|
121 |
+
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
122 |
+
}
|
123 |
+
|
124 |
+
hr {
|
125 |
+
//height: 0.0625rem;
|
126 |
+
height: 1px;
|
127 |
+
width: 100%;
|
128 |
+
background-color: var(--border-color);
|
129 |
+
display: block;
|
130 |
+
margin: 1.5rem 0;
|
131 |
+
}
|
132 |
+
|
133 |
+
h2 {
|
134 |
+
font-size: 1.59375rem;
|
135 |
+
line-height: 1.25;
|
136 |
+
font-weight: 500;
|
137 |
+
color: var(--text-color);
|
138 |
+
padding-bottom: 1.5rem;
|
139 |
+
}
|
src/types.d.ts
ADDED
File without changes
|
src/utils/dates.ts
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const iso8601ToFrenchRegex = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/;
|
2 |
+
export function iso8601ToFrench(iso8601: string): string {
|
3 |
+
console.log("iso8601ToTokens", iso8601)
|
4 |
+
|
5 |
+
const matches = iso8601.match(iso8601ToFrenchRegex);
|
6 |
+
|
7 |
+
const year = matches[1];
|
8 |
+
const month = months[parseInt(matches[2], 10) - 1];
|
9 |
+
const day = matches[3];
|
10 |
+
const hours = matches[4];
|
11 |
+
const minutes = matches[5];
|
12 |
+
const seconds = matches[6];
|
13 |
+
|
14 |
+
return `${day} ${month} ${year} à ${hours}:${minutes}:${seconds}`;
|
15 |
+
}
|
16 |
+
|
17 |
+
const frenchToIso8601Regex = /(\d{1,2}) ([a-zA-Z\u00C0-\u024F]+) (\d{4}) à (\d{2}):(\d{2}):(\d{2})/;
|
18 |
+
export function frenchToIso8601(french: string): string {
|
19 |
+
console.log("tokensToIso8601", french)
|
20 |
+
// Match the components of the date and time from the input string
|
21 |
+
|
22 |
+
// const months = {
|
23 |
+
// janvier: '01', février: '02', mars: '03', avril: '04', mai: '05',
|
24 |
+
// juin: '06', juillet: '07', août: '08', septembre: '09',
|
25 |
+
// octobre: '10', novembre: '11', décembre: '12'
|
26 |
+
// };
|
27 |
+
|
28 |
+
const match = french.match(frenchToIso8601Regex);
|
29 |
+
|
30 |
+
if (!match) {
|
31 |
+
throw new Error('Invalid date format');
|
32 |
+
}
|
33 |
+
|
34 |
+
const [, day, month, year, hours, minutes, seconds] = match;
|
35 |
+
|
36 |
+
// Convert the month name to a numeric format
|
37 |
+
const monthNumber = (months.indexOf(month) + 1).toString();
|
38 |
+
|
39 |
+
if (!monthNumber) {
|
40 |
+
throw new Error('Invalid month name');
|
41 |
+
}
|
42 |
+
|
43 |
+
// Construct the ISO 8601 formatted string
|
44 |
+
const isoDate = `${year}-${monthNumber.padStart(2, '0')}-${day.padStart(2, '0')}T${hours}:${minutes}:${seconds}`;
|
45 |
+
return isoDate;
|
46 |
+
}
|
47 |
+
|
48 |
+
const months = [
|
49 |
+
"janvier", "février", "mars", "avril", "mai", "juin",
|
50 |
+
"juillet", "août", "septembre", "octobre", "novembre", "décembre"
|
51 |
+
];
|
src/utils/model.ts
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Post, Topic} from "./topics";
|
2 |
+
import {iso8601ToFrench, frenchToIso8601} from "./dates"
|
3 |
+
import {generateUUID} from "./uuids";
|
4 |
+
|
5 |
+
// const titleRegex = /Sujet\s+:\s+"([^"]+)"/;
|
6 |
+
const titleRegex = /Sujet\s+:\s+"(.+?)"?<\|eot_id\|>/;
|
7 |
+
const userRegex = /<\|im_pseudo\|>([^<]+)<\|end_pseudo\|>/;
|
8 |
+
const dateRegex = /<\|im_date\|>([^<]+)<\|end_date\|>/;
|
9 |
+
const contentRegex = /<\|begin_of_post\|>([\s\S]+)(?:<\|end_of_post\|>)?$/;
|
10 |
+
|
11 |
+
export function tokensToTopic(tokens: string): Topic {
|
12 |
+
const topic: Topic = {
|
13 |
+
id: generateUUID(),
|
14 |
+
title: "",
|
15 |
+
posts: [],
|
16 |
+
};
|
17 |
+
|
18 |
+
// const splits = tokens.split("<|end_of_post|>")
|
19 |
+
// console.log("Splits:")
|
20 |
+
// console.log(splits);
|
21 |
+
|
22 |
+
// Split token in posts
|
23 |
+
// The last element is always vois, so remove it
|
24 |
+
for(const postTokens of tokens.split("<|end_of_post|>").slice(0, -1)) {
|
25 |
+
console.log("Post tokens:")
|
26 |
+
console.log(postTokens);
|
27 |
+
|
28 |
+
// If it's the first post
|
29 |
+
if(topic.posts.length < 1) {
|
30 |
+
const titleMatch = postTokens.match(titleRegex);
|
31 |
+
console.log(`title: ${titleMatch[1]}`)
|
32 |
+
|
33 |
+
topic.title = titleMatch[1];
|
34 |
+
}
|
35 |
+
|
36 |
+
// topic.posts.push(tokensToPosts());
|
37 |
+
topic.posts = topic.posts.concat(tokensToPosts(postTokens));
|
38 |
+
}
|
39 |
+
|
40 |
+
return topic;
|
41 |
+
}
|
42 |
+
|
43 |
+
export function tokensToPosts(tokens: string): Post[] {
|
44 |
+
const posts: Post[] = [];
|
45 |
+
|
46 |
+
for(const postTokens of tokens.split("<|end_of_post|>")) {
|
47 |
+
|
48 |
+
// TODO: remove the last instead of doing this, because the last can be incomplete
|
49 |
+
if(postTokens.length < 1) {
|
50 |
+
continue;
|
51 |
+
}
|
52 |
+
|
53 |
+
console.log("Post tokens:")
|
54 |
+
console.log(postTokens);
|
55 |
+
|
56 |
+
const userMatch = postTokens.match(userRegex);
|
57 |
+
console.log(`user: ${userMatch[1]}`)
|
58 |
+
|
59 |
+
const dateMatch = postTokens.match(dateRegex);
|
60 |
+
console.log(`date: ${dateMatch[1]}`)
|
61 |
+
|
62 |
+
const contentMatch = postTokens.match(contentRegex);
|
63 |
+
console.log(`content: ${contentMatch[1]}`)
|
64 |
+
|
65 |
+
posts.push({
|
66 |
+
user: userMatch[1],
|
67 |
+
date: frenchToIso8601(dateMatch[1]),
|
68 |
+
content: contentMatch[1],
|
69 |
+
});
|
70 |
+
}
|
71 |
+
|
72 |
+
return posts;
|
73 |
+
}
|
74 |
+
|
75 |
+
|
76 |
+
export function tokenizeTopic(topic: Topic): string {
|
77 |
+
if (topic.posts.length === 0) {
|
78 |
+
throw new Error("Topic must have at least one post")
|
79 |
+
}
|
80 |
+
|
81 |
+
const tokenizedPosts = topic.posts.map(post => tokenizePost(post, topic.posts[0].user)).flat().join("");
|
82 |
+
|
83 |
+
// console.log("Tokenized posts:")
|
84 |
+
// console.log(tokenizedPosts)
|
85 |
+
|
86 |
+
let lines = [
|
87 |
+
"<|start_header_id|><|sujet|><|end_header_id|>",
|
88 |
+
"",
|
89 |
+
`Sujet : "${topic.title}"`,
|
90 |
+
];
|
91 |
+
|
92 |
+
return lines.join("\n") + tokenizedPosts;
|
93 |
+
}
|
94 |
+
|
95 |
+
export function tokenizePost(post: Post, poster: string): string {
|
96 |
+
let lines = [
|
97 |
+
`<|eot_id|><|start_header_id|><|${post.user === poster ? "autheur" : "khey"}|>`,
|
98 |
+
"<|end_header_id|>",
|
99 |
+
"",
|
100 |
+
`<|im_pseudo|>${post.user}<|end_pseudo|>`,
|
101 |
+
`<|im_date|>Le ${iso8601ToFrench(post.date)}<|end_date|>`,
|
102 |
+
"",
|
103 |
+
`<|begin_of_post|>${post.content}<|end_of_post|>`
|
104 |
+
];
|
105 |
+
|
106 |
+
return lines.join("\n");
|
107 |
+
}
|
src/utils/oobabooga.ts
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Post, Topic} from "./topics";
|
2 |
+
import {Settings} from "./settings";
|
3 |
+
import {generateUUID} from "./uuids";
|
4 |
+
import {tokenizeTopic, tokensToPosts, tokensToTopic} from "./model";
|
5 |
+
// import {replaceSmileysInText} from "./smileys";
|
6 |
+
//
|
7 |
+
// try {
|
8 |
+
// console.log(replaceSmileysInText("lol"))
|
9 |
+
// }catch(e) {}
|
10 |
+
|
11 |
+
// @see https://github.com/openai/openai-node/blob/14784f95797d4d525dafecfd4ec9c7a133540da0/src/resources/chat/completions.ts
|
12 |
+
type OobaboogaStreamChunk = {
|
13 |
+
id: string; // Unique identifier for the chunk
|
14 |
+
object: string; // The type of the chunk, e.g., "text_completion.chunk"
|
15 |
+
created: number; // Unix timestamp of when the chunk was created
|
16 |
+
model: string; // Name or identifier of the model generating the completion
|
17 |
+
choices: {
|
18 |
+
index: number; // The index of the choice in the completion
|
19 |
+
finish_reason: string | null; // Reason why the completion stopped, or null if still in progress
|
20 |
+
text: string; // The generated text for this chunk
|
21 |
+
logprobs: {
|
22 |
+
top_logprobs: Record<string, number>[]; // Log probabilities for the top tokens, as an array of key-value pairs
|
23 |
+
};
|
24 |
+
}[];
|
25 |
+
usage?: {
|
26 |
+
prompt_tokens: number; // Number of tokens in the prompt
|
27 |
+
completion_tokens: number; // Number of tokens generated in the completion
|
28 |
+
total_tokens: number; // Total tokens used
|
29 |
+
};
|
30 |
+
};
|
31 |
+
|
32 |
+
export async function generateTopic(settings: Settings, nPosts: number): Promise<Topic> {
|
33 |
+
console.log(settings);
|
34 |
+
const rawOutput = await fetApiWithStream(settings, "<|start_header_id|>", nPosts);
|
35 |
+
// const rawOutput = await fetApi(settings);
|
36 |
+
// console.log(rawOutput);
|
37 |
+
// let rawOutput = "rawOutput";
|
38 |
+
|
39 |
+
return tokensToTopic(rawOutput);
|
40 |
+
}
|
41 |
+
|
42 |
+
export async function generatePosts(settings: Settings, nPosts: number, topic: Topic): Promise<Post[]> {
|
43 |
+
// console.log(settings);
|
44 |
+
const rawOutput = await fetApiWithStream(settings, tokenizeTopic(topic), nPosts);
|
45 |
+
// const rawOutput = await fetApi(settings);
|
46 |
+
// console.log(rawOutput);
|
47 |
+
// let rawOutput = "rawOutput";
|
48 |
+
|
49 |
+
console.log("rawOutput");
|
50 |
+
console.log(rawOutput);
|
51 |
+
|
52 |
+
return tokensToPosts(rawOutput);
|
53 |
+
}
|
54 |
+
|
55 |
+
|
56 |
+
|
57 |
+
|
58 |
+
async function fetApi(settings: Settings): Promise<string> {
|
59 |
+
const response = await fetch(new URL("/v1/completions", settings.apiURL), {
|
60 |
+
method: "POST",
|
61 |
+
headers: {
|
62 |
+
"Content-Type": "application/json",
|
63 |
+
},
|
64 |
+
body: JSON.stringify({
|
65 |
+
prompt: "<|start_header_id|>",
|
66 |
+
temperature: settings.temperature,
|
67 |
+
max_tokens: 1000,
|
68 |
+
stream: false,
|
69 |
+
skip_special_tokens: false,
|
70 |
+
stop: "<|end_of_post|>"
|
71 |
+
// top_p: 1,
|
72 |
+
// frequency_penalty: 0,
|
73 |
+
// presence_penalty: 0,
|
74 |
+
}),
|
75 |
+
});
|
76 |
+
|
77 |
+
if (response.status !== 200) {
|
78 |
+
throw new Error(`Failed to fetch API (${response.status}): ${response.statusText}`);
|
79 |
+
}
|
80 |
+
|
81 |
+
const json = await response.json();
|
82 |
+
|
83 |
+
console.log(json)
|
84 |
+
|
85 |
+
return json.choices[0].text;
|
86 |
+
}
|
87 |
+
|
88 |
+
const postEndToken = "<|end_of_post|>";
|
89 |
+
|
90 |
+
// @see https://github.com/openai/openai-node/issues/18
|
91 |
+
// nPosts: number of post before stop
|
92 |
+
async function fetApiWithStream(settings: Settings, prompt: string, nPosts: number): Promise<string> {
|
93 |
+
const controller = new AbortController()
|
94 |
+
const response = await fetch(new URL("/v1/completions", settings.apiURL), {
|
95 |
+
method: "POST",
|
96 |
+
headers: {
|
97 |
+
"Content-Type": "application/json",
|
98 |
+
},
|
99 |
+
body: JSON.stringify({
|
100 |
+
prompt,
|
101 |
+
temperature: settings.temperature,
|
102 |
+
max_tokens: 2000,
|
103 |
+
stream: true,
|
104 |
+
skip_special_tokens: false,
|
105 |
+
// stop: "<|end_of_post|>"
|
106 |
+
// top_p: 1,
|
107 |
+
// frequency_penalty: 0,
|
108 |
+
// presence_penalty: 0,
|
109 |
+
}),
|
110 |
+
signal: controller.signal,
|
111 |
+
});
|
112 |
+
|
113 |
+
if (!response.ok) {
|
114 |
+
throw new Error(`Failed to fetch API (${response.status} ${response.statusText}): ${await response.text()}`);
|
115 |
+
}
|
116 |
+
|
117 |
+
// console.log("Streaming !!!!");
|
118 |
+
//
|
119 |
+
// const decoderStream = new TextDecoderStream("utf-8");
|
120 |
+
// const writer = new WritableStream({
|
121 |
+
// write(rawChunk: string) {
|
122 |
+
// // output.innerHTML += chunk;
|
123 |
+
// const chunk = JSON.parse(rawChunk.trimStart().slice(6)) as OobaboogaStreamChunk; // remove "data: " and parse
|
124 |
+
// console.log(chunk)
|
125 |
+
// }
|
126 |
+
// });
|
127 |
+
|
128 |
+
console.log(`Fetching topic with ${nPosts} posts...`);
|
129 |
+
|
130 |
+
let endTokenCount = 0;
|
131 |
+
let tokens = ""; // Dont know why but the first token is skipped
|
132 |
+
let finishReason: string | null = null;
|
133 |
+
|
134 |
+
try {
|
135 |
+
await response.body.pipeThrough(new TextDecoderStream("utf-8")).pipeTo(new WritableStream({
|
136 |
+
write(rawChunk: string) {
|
137 |
+
// chunk can contains multiple lines, one chunk data per line
|
138 |
+
for (const rawChunkLine of rawChunk.split("\n")) {
|
139 |
+
if (!rawChunkLine.startsWith("data:")) continue;
|
140 |
+
const chunk = JSON.parse(rawChunkLine.slice(6)) as OobaboogaStreamChunk; // remove "data: " and parse
|
141 |
+
const text = chunk.choices[0].text;
|
142 |
+
console.log(text)
|
143 |
+
tokens += chunk.choices[0].text;
|
144 |
+
if (text.includes(postEndToken)) {
|
145 |
+
endTokenCount++;
|
146 |
+
|
147 |
+
if(endTokenCount >= nPosts) {
|
148 |
+
finishReason = "custom_stop";
|
149 |
+
controller.abort();
|
150 |
+
break;
|
151 |
+
}
|
152 |
+
} else {
|
153 |
+
finishReason = chunk.choices[0].finish_reason;
|
154 |
+
}
|
155 |
+
}
|
156 |
+
// output.innerHTML += chunk;
|
157 |
+
// console.log("----")
|
158 |
+
// console.log(rawChunk)
|
159 |
+
// console.log(rawChunk.slice(6).trimEnd())
|
160 |
+
|
161 |
+
// console.log(chunk.choices[0].text)
|
162 |
+
// tokens += chunk.choices[0].text;
|
163 |
+
}
|
164 |
+
}));
|
165 |
+
} catch (e) {
|
166 |
+
if (e.name !== 'AbortError') {
|
167 |
+
throw e;
|
168 |
+
}
|
169 |
+
}
|
170 |
+
|
171 |
+
console.log("Done fetching data")
|
172 |
+
console.log(`Finish reason: ${finishReason}`)
|
173 |
+
console.log(`Tokens: ${tokens}`)
|
174 |
+
|
175 |
+
return tokens;
|
176 |
+
}
|
src/utils/route.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const routes = {
|
2 |
+
home: "home",
|
3 |
+
topic: "topic",
|
4 |
+
settings: "settings",
|
5 |
+
} as const;
|
6 |
+
|
7 |
+
export type Route = typeof routes[keyof typeof routes];
|
8 |
+
|
9 |
+
export type RouteSetter = (route: Route, page?: number, id?: string) => void;
|
src/utils/settings.ts
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const itemKey = "settings";
|
2 |
+
|
3 |
+
export type Settings = {
|
4 |
+
apiURL: string;
|
5 |
+
temperature: number;
|
6 |
+
postCount: number;
|
7 |
+
}
|
8 |
+
|
9 |
+
const defaultSettings: Settings = {
|
10 |
+
apiURL: "http://localhost:8000",
|
11 |
+
temperature: 0.75,
|
12 |
+
postCount: 3,
|
13 |
+
}
|
14 |
+
|
15 |
+
export function fetchSettings(): Settings {
|
16 |
+
const storedSettings = localStorage.getItem(itemKey);
|
17 |
+
if (storedSettings) {
|
18 |
+
return {...defaultSettings, ...JSON.parse(storedSettings)} as Settings;
|
19 |
+
} else {
|
20 |
+
return defaultSettings;
|
21 |
+
}
|
22 |
+
}
|
23 |
+
|
24 |
+
export function saveSettings(settings: Settings) {
|
25 |
+
localStorage.setItem(itemKey, JSON.stringify(settings));
|
26 |
+
}
|
src/utils/smileys.ts
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const smileysMap: [string, string][] = [
|
2 |
+
[":)", "1"], // https://image.jeuxvideo.com/smileys_img/1.gif
|
3 |
+
[":snif:", "20"],
|
4 |
+
[":gba:", "17"],
|
5 |
+
[":g)", "3"],
|
6 |
+
[":-)", "46"],
|
7 |
+
[":snif2:", "13"],
|
8 |
+
[":bravo:", "69"],
|
9 |
+
[":d)", "4"],
|
10 |
+
[":hap:", "18"],
|
11 |
+
[":ouch:", "22"],
|
12 |
+
[":pacg:", "9"],
|
13 |
+
[":cd:", "5"],
|
14 |
+
[":-)))", "23"],
|
15 |
+
[":ouch2:", "57"],
|
16 |
+
[":pacd:", "10"],
|
17 |
+
[":cute:", "nyu"],
|
18 |
+
[":content:", "24"],
|
19 |
+
[":p)", "7"],
|
20 |
+
[":-p", "31"],
|
21 |
+
[":noel:", "11"],
|
22 |
+
[":oui:", "37"],
|
23 |
+
[":(", "45"],
|
24 |
+
[":peur:", "47"],
|
25 |
+
[":question:", "2"],
|
26 |
+
[":cool:", "26"],
|
27 |
+
[":-(", "14"],
|
28 |
+
[":coeur:", "54"],
|
29 |
+
[":mort:", "21"],
|
30 |
+
[":rire:", "39"],
|
31 |
+
[":-((", "15"],
|
32 |
+
[":fou:", "50"],
|
33 |
+
[":sleep:", "27"],
|
34 |
+
[":-D", "40"],
|
35 |
+
[":nonnon:", "25"],
|
36 |
+
[":fier:", "53"],
|
37 |
+
[":honte:", "30"],
|
38 |
+
[":rire2:", "41"],
|
39 |
+
[":non2:", "33"],
|
40 |
+
[":sarcastic:", "43"],
|
41 |
+
[":monoeil:", "34"],
|
42 |
+
[":o))", "12"],
|
43 |
+
[":nah:", "19"],
|
44 |
+
[":doute:", "28"],
|
45 |
+
[":rouge:", "55"],
|
46 |
+
[":ok:", "36"],
|
47 |
+
[":non:", "35"],
|
48 |
+
[":malade:", "8"],
|
49 |
+
[":fete:", "66"],
|
50 |
+
[":sournois:", "67"],
|
51 |
+
[":hum:", "68"],
|
52 |
+
[":ange:", "60"],
|
53 |
+
[":diable:", "61"],
|
54 |
+
[":gni:", "62"],
|
55 |
+
[":play:", "play"],
|
56 |
+
[":desole:", "65"],
|
57 |
+
[":spoiler:", "63"],
|
58 |
+
[":merci:", "58"],
|
59 |
+
[":svp:", "59"],
|
60 |
+
[":sors:", "56"],
|
61 |
+
[":salut:", "42"],
|
62 |
+
[":rechercher:", "38"],
|
63 |
+
[":hello:", "29"],
|
64 |
+
[":up:", "44"],
|
65 |
+
[":bye:", "48"],
|
66 |
+
[":gne:", "51"],
|
67 |
+
[":lol:", "32"],
|
68 |
+
[":dpdr:", "49"],
|
69 |
+
[":dehors:", "52"],
|
70 |
+
[":hs:", "64"],
|
71 |
+
[":banzai:", "70"],
|
72 |
+
[":bave:", "71"],
|
73 |
+
[":pf:", "pf"],
|
74 |
+
[":cimer:", "cimer"],
|
75 |
+
[":ddb:", "ddb"],
|
76 |
+
[":pave:", "pave"],
|
77 |
+
[":objection:", "objection"],
|
78 |
+
[":siffle:", "siffle"],
|
79 |
+
];
|
src/utils/topics.ts
ADDED
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export type Post = {
|
2 |
+
user: string;
|
3 |
+
date: string; // ISO 8601, YYYY-MM-DDTHH:MM:SS
|
4 |
+
content: string;
|
5 |
+
}
|
6 |
+
|
7 |
+
export type Topic = {
|
8 |
+
id: string; // UUID
|
9 |
+
title: string;
|
10 |
+
posts: Post[];
|
11 |
+
}
|
12 |
+
|
13 |
+
const itemKey = "topics";
|
14 |
+
|
15 |
+
export function loadTopics(): Topic[] {
|
16 |
+
const storedTopics = localStorage.getItem(itemKey);
|
17 |
+
if (storedTopics) {
|
18 |
+
return JSON.parse(storedTopics) as Topic[];
|
19 |
+
} else {
|
20 |
+
return [];
|
21 |
+
}
|
22 |
+
}
|
23 |
+
|
24 |
+
export function saveTopics(topics: Topic[]) {
|
25 |
+
localStorage.setItem(itemKey, JSON.stringify(topics));
|
26 |
+
}
|
27 |
+
|
28 |
+
|
29 |
+
const testTopics: Topic[] = [
|
30 |
+
{
|
31 |
+
id: "dbf575b3-fc93-47b8-8773-bd91692dc97c",
|
32 |
+
title: "Les néerlandais font tous 2m",
|
33 |
+
posts: [
|
34 |
+
{
|
35 |
+
user: "elegante12",
|
36 |
+
date: "2025-01-15T09:13:44",
|
37 |
+
content: "https://vocaroo.com/1cCsqH5ai8AW\nN'allais pas ma bas si vous faites pas mini 1m80 même les meufs sont dans les 1m75 minimum"
|
38 |
+
},
|
39 |
+
{
|
40 |
+
user: "Pajoad",
|
41 |
+
date: "2025-01-15T09:14:45",
|
42 |
+
content: "Je suis (oui c'est on EST notre taille) 1m73 et j'ai été à Amsterdam en 2022, j'ai eu l'impression d'avoir 10 ans"
|
43 |
+
},
|
44 |
+
{
|
45 |
+
user: "Qui-es-tu",
|
46 |
+
date: "2025-01-15T09:15:13",
|
47 |
+
content: "Un enfer sur terre quoi."
|
48 |
+
},
|
49 |
+
]
|
50 |
+
},
|
51 |
+
{
|
52 |
+
id: "0dbfad91-c5b7-4e11-a41f-eae8162ddfa5",
|
53 |
+
title: "C'est normal qu'une fille pète au lit ?",
|
54 |
+
posts: [
|
55 |
+
{
|
56 |
+
user: "Muskis",
|
57 |
+
date: "2025-01-15T10:47:14",
|
58 |
+
content: "Quand on couche avec une fille c'est normal qu'elle pète au lit avant ou après l'amour en mode balécouilles que tu sois là ?"
|
59 |
+
},
|
60 |
+
{
|
61 |
+
user: "edreMaLeuPcvJ-4",
|
62 |
+
date: "2025-01-15T10:49:45",
|
63 |
+
content: "Non, les filles ne pètent pas.\nSi elle pète c'est qu'elle est trans :ok:"
|
64 |
+
},
|
65 |
+
{
|
66 |
+
user: "PereAttali",
|
67 |
+
date: "2025-01-15T10:49:37",
|
68 |
+
content: "Quand tu lui fous dans le fion et que ta queue est trop large c'est ce qui peut arriver (lié à la dilatation rapide)"
|
69 |
+
},
|
70 |
+
{
|
71 |
+
user: "Pajoad",
|
72 |
+
date: "2025-01-15T10:56:37",
|
73 |
+
content: "Ça a quelle odeur d'ailleurs un pet féminin ?"
|
74 |
+
},
|
75 |
+
{
|
76 |
+
user: "hypertendu",
|
77 |
+
date: "2025-01-15T10:58:15",
|
78 |
+
content: "Ça peut être un pet vaginal :("
|
79 |
+
},
|
80 |
+
]
|
81 |
+
},
|
82 |
+
{
|
83 |
+
id: "816dbd52-3f8e-4e44-9b8a-fcc388485e9f",
|
84 |
+
title: "Ta cousine veut plaquer ses FEETS sur ton visage",
|
85 |
+
posts: [
|
86 |
+
{
|
87 |
+
user: "BanDef411",
|
88 |
+
date: "2025-01-15T10:42:00",
|
89 |
+
content: "Es-tu préparé face a l’odeur? :("
|
90 |
+
},
|
91 |
+
{
|
92 |
+
user: "McLoviin",
|
93 |
+
date: "2025-01-15T10:43:35",
|
94 |
+
content: "Etant donné qu'elle me fait bander comme un taureau depuis l'adolescence je suis son tapis, son fauteuil, tout est dispo pour elle."
|
95 |
+
},
|
96 |
+
]
|
97 |
+
},
|
98 |
+
{
|
99 |
+
id: "e1504b59-3918-4ee6-bced-efcd3af3f278",
|
100 |
+
title: "[CDD] Je BAISE ta FEMME contre REMUNERATION",
|
101 |
+
posts: [
|
102 |
+
{
|
103 |
+
user: "LeFeetBossV3",
|
104 |
+
date: "2025-01-15T11:04:30",
|
105 |
+
content: "Vu qu'apparement tu sais pas la satisfaire, pour 500 balles je la baise vite fait https://image.noelshack.com/minis/2018/25/2/1529422413-risitaszoom.png\n\nElle ira mieux et votre couple sera sauvé https://image.noelshack.com/minis/2018/25/2/1529422413-risitaszoom.png"
|
106 |
+
},
|
107 |
+
{
|
108 |
+
user: "Platitude24",
|
109 |
+
date: "2025-01-15T11:05:51",
|
110 |
+
content: "Ma femme possède un pénis féminin et elle demande si elle peut te sodomiser ?"
|
111 |
+
},
|
112 |
+
]
|
113 |
+
},
|
114 |
+
{
|
115 |
+
id: "e5c288db-45a0-43e0-aa37-df485da475e7",
|
116 |
+
title: "La policière au gros CUL de Marseille est devenu MILLIONNAIRE",
|
117 |
+
posts: [
|
118 |
+
{
|
119 |
+
user: "Samanthabagare3",
|
120 |
+
date: "2025-01-15T11:59:32",
|
121 |
+
content: "https://image.noelshack.com/fichiers/2025/03/3/1736938679-screenshot-2025-01-15-11-57-10-521-com-instagram-android-edit.jpg\nhttps://image.noelshack.com/minis/2025/03/3/1736938679-screenshot-2025-01-15-11-57-10-521-com-instagram-android-edit.png\nEntrepreneuriat\" = Mym + onlyfan https://image.noelshack.com/fichiers/2018/25/2/1529422413-risitaszoom.png\n\nFrom 500 abonnés to 232 000 grâce au buzz https://image.noelshack.com/fichiers/2018/25/2/1529422413-risitaszoom.png"
|
122 |
+
},
|
123 |
+
{
|
124 |
+
user: "Platitude24",
|
125 |
+
date: "2025-01-15T11:59:36",
|
126 |
+
content: "Comment ça peut être policière ça ? :("
|
127 |
+
},
|
128 |
+
{
|
129 |
+
user: "Samanthabagare3",
|
130 |
+
date: "2025-01-15T12:00:02",
|
131 |
+
content: "> Le 15 janvier 2025 à 11:59:36 :\n> Comment ça peut être policière ça ?\n\nAHI <spoil>loool</spoil> AHI\nShitholisation du pays https://image.noelshack.com/fichiers/2018/25/2/1529422413-risitaszoom.png"
|
132 |
+
},
|
133 |
+
]
|
134 |
+
},
|
135 |
+
];
|
src/utils/uuids.ts
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function generateUUID(): string {
|
2 |
+
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
3 |
+
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
4 |
+
);
|
5 |
+
}
|
style.css
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
body {
|
2 |
-
padding: 2rem;
|
3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
4 |
-
}
|
5 |
-
|
6 |
-
h1 {
|
7 |
-
font-size: 16px;
|
8 |
-
margin-top: 0;
|
9 |
-
}
|
10 |
-
|
11 |
-
p {
|
12 |
-
color: rgb(107, 114, 128);
|
13 |
-
font-size: 15px;
|
14 |
-
margin-bottom: 10px;
|
15 |
-
margin-top: 5px;
|
16 |
-
}
|
17 |
-
|
18 |
-
.card {
|
19 |
-
max-width: 620px;
|
20 |
-
margin: 0 auto;
|
21 |
-
padding: 16px;
|
22 |
-
border: 1px solid lightgray;
|
23 |
-
border-radius: 16px;
|
24 |
-
}
|
25 |
-
|
26 |
-
.card p:last-child {
|
27 |
-
margin-bottom: 0;
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tsconfig.json
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "ES2020",
|
4 |
+
"module": "ESNext",
|
5 |
+
"moduleResolution": "bundler",
|
6 |
+
"noEmit": true,
|
7 |
+
"allowJs": true,
|
8 |
+
"checkJs": true,
|
9 |
+
|
10 |
+
/* Preact Config */
|
11 |
+
"jsx": "react-jsx",
|
12 |
+
"jsxImportSource": "preact",
|
13 |
+
"skipLibCheck": true,
|
14 |
+
"paths": {
|
15 |
+
"react": ["./node_modules/preact/compat/"],
|
16 |
+
"react-dom": ["./node_modules/preact/compat/"]
|
17 |
+
},
|
18 |
+
"allowSyntheticDefaultImports": true
|
19 |
+
},
|
20 |
+
"include": ["node_modules/vite/client.d.ts", "**/*"]
|
21 |
+
}
|
vite.config.ts
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {defineConfig} from 'vite';
|
2 |
+
// import {patchCssModules} from "vite-css-modules"
|
3 |
+
import preact from '@preact/preset-vite';
|
4 |
+
|
5 |
+
// https://vitejs.dev/config/
|
6 |
+
export default defineConfig({
|
7 |
+
plugins: [
|
8 |
+
// patchCssModules(),
|
9 |
+
preact({ // I had to disable HMR due to un Vite bug (@see https://github.com/privatenumber/vite-css-modules)
|
10 |
+
devToolsEnabled: false,
|
11 |
+
reactAliasesEnabled: false,
|
12 |
+
prefreshEnabled: false
|
13 |
+
})
|
14 |
+
],
|
15 |
+
build: {
|
16 |
+
target: "es2022"
|
17 |
+
},
|
18 |
+
});
|