zouhairk commited on
Commit
b681a44
·
1 Parent(s): 63eff25

init front

Browse files
front/.bolt/config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "template": "bolt-vite-react-ts"
3
+ }
front/.bolt/prompt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
2
+
3
+ By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
4
+
5
+ Use icons from lucide-react for logos.
6
+
7
+ Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.
8
+
front/.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?
front/eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import reactHooks from 'eslint-plugin-react-hooks';
4
+ import reactRefresh from 'eslint-plugin-react-refresh';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ }
28
+ );
front/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>MSA translator</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
front/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
front/package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "voice-chat-translation-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@microsoft/fetch-event-source": "^2.0.1",
14
+ "lucide-react": "^0.344.0",
15
+ "react": "^18.3.1",
16
+ "react-dom": "^18.3.1",
17
+ "react-router-dom": "^6.22.3"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.9.1",
21
+ "@types/react": "^18.3.5",
22
+ "@types/react-dom": "^18.3.0",
23
+ "@vitejs/plugin-react": "^4.3.1",
24
+ "autoprefixer": "^10.4.18",
25
+ "eslint": "^9.9.1",
26
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
27
+ "eslint-plugin-react-refresh": "^0.4.11",
28
+ "globals": "^15.9.0",
29
+ "postcss": "^8.4.35",
30
+ "tailwindcss": "^3.4.1",
31
+ "typescript": "^5.5.3",
32
+ "typescript-eslint": "^8.3.0",
33
+ "vite": "^5.4.2"
34
+ }
35
+ }
front/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
front/src/App.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
3
+ import { Globe2 } from 'lucide-react';
4
+ import LanguageSelection from './pages/LanguageSelection';
5
+ import ArabicChat from './pages/ArabicChat';
6
+ import FrenchChat from './pages/FrenchChat';
7
+ import { ChatProvider } from './context/ChatContext';
8
+
9
+ function App() {
10
+ return (
11
+ <ChatProvider>
12
+ <Router>
13
+ <div className="min-h-screen bg-[#F6F6F6]">
14
+ <nav className="bg-white shadow-md p-4">
15
+ <div className="container mx-auto flex items-center justify-between">
16
+ <Link to="/" className="flex items-center space-x-2">
17
+ <Globe2 className="h-6 w-6 text-blue-600" />
18
+ <span className="text-xl font-semibold">MSA Translator</span>
19
+ </Link>
20
+ <div className="flex space-x-4">
21
+ <Link to="/" className="px-4 py-2 rounded hover:bg-gray-100">Home</Link>
22
+ <Link to="/arabic" className="px-4 py-2 rounded hover:bg-gray-100">Arabic Chat</Link>
23
+ <Link to="/french" className="px-4 py-2 rounded hover:bg-gray-100">French Chat</Link>
24
+ </div>
25
+ </div>
26
+ </nav>
27
+
28
+ <Routes>
29
+ <Route path="/" element={<LanguageSelection />} />
30
+ <Route path="/arabic" element={<ArabicChat />} />
31
+ <Route path="/french" element={<FrenchChat />} />
32
+ </Routes>
33
+ </div>
34
+ </Router>
35
+ </ChatProvider>
36
+ );
37
+ }
38
+
39
+ export default App;
front/src/context/ChatContext.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useState } from 'react';
2
+
3
+ interface Message {
4
+ id: number;
5
+ text: string;
6
+ isArabic?: boolean;
7
+ isFrench?: boolean;
8
+ timestamp: Date;
9
+ }
10
+
11
+ interface ChatContextType {
12
+ messages: Message[];
13
+ addMessage: (message: Message) => void;
14
+ }
15
+
16
+ const ChatContext = createContext<ChatContextType | undefined>(undefined);
17
+
18
+ export function ChatProvider({ children }: { children: React.ReactNode }) {
19
+ const [messages, setMessages] = useState<Message[]>([]);
20
+
21
+ const addMessage = (message: Message) => {
22
+ setMessages(prev => [...prev, message]);
23
+ };
24
+
25
+ return (
26
+ <ChatContext.Provider value={{ messages, addMessage }}>
27
+ {children}
28
+ </ChatContext.Provider>
29
+ );
30
+ }
31
+
32
+ export function useChat() {
33
+ const context = useContext(ChatContext);
34
+ if (context === undefined) {
35
+ throw new Error('useChat must be used within a ChatProvider');
36
+ }
37
+ return context;
38
+ }
front/src/index.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
front/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
front/src/pages/ArabicChat.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import { Mic, Play, Square } from 'lucide-react';
3
+ import { useChat } from '../context/ChatContext';
4
+
5
+ const ArabicChat = () => {
6
+ const { messages, addMessage } = useChat();
7
+ const [isRecording, setIsRecording] = useState(false);
8
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null);
9
+ const chunksRef = useRef<Blob[]>([]);
10
+ const recognitionRef = useRef<SpeechRecognition | null>(null);
11
+
12
+ const startRecording = async () => {
13
+ try {
14
+ // Initialize speech recognition
15
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
16
+ const recognition = new SpeechRecognition();
17
+ recognitionRef.current = recognition;
18
+
19
+ recognition.lang = 'ar-SA';
20
+ recognition.continuous = true;
21
+ recognition.interimResults = true;
22
+
23
+ recognition.onresult = (event) => {
24
+ const transcript = Array.from(event.results)
25
+ .map(result => result[0].transcript)
26
+ .join('');
27
+
28
+ if (event.results[0].isFinal) {
29
+ const newMessage = {
30
+ id: Date.now(),
31
+ text: transcript,
32
+ isArabic: true,
33
+ timestamp: new Date(),
34
+ };
35
+ addMessage(newMessage);
36
+ }
37
+ };
38
+
39
+ recognition.start();
40
+ setIsRecording(true);
41
+
42
+ // Also start audio recording for WAV file
43
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
44
+ const mediaRecorder = new MediaRecorder(stream);
45
+ mediaRecorderRef.current = mediaRecorder;
46
+ chunksRef.current = [];
47
+
48
+ mediaRecorder.ondataavailable = (e) => {
49
+ chunksRef.current.push(e.data);
50
+ };
51
+
52
+ mediaRecorder.start();
53
+ } catch (err) {
54
+ console.error('Error accessing microphone:', err);
55
+ }
56
+ };
57
+
58
+ const stopRecording = () => {
59
+ if (recognitionRef.current) {
60
+ recognitionRef.current.stop();
61
+ }
62
+
63
+ if (mediaRecorderRef.current && isRecording) {
64
+ mediaRecorderRef.current.stop();
65
+ setIsRecording(false);
66
+ mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
67
+ }
68
+ };
69
+
70
+ const speakText = (text: string, isArabic: boolean) => {
71
+ if ('speechSynthesis' in window) {
72
+ window.speechSynthesis.cancel();
73
+
74
+ const utterance = new SpeechSynthesisUtterance(text);
75
+ utterance.lang = isArabic ? 'ar-SA' : 'fr-FR';
76
+ utterance.rate = 1.0;
77
+ utterance.pitch = 1.0;
78
+ utterance.volume = 1.0;
79
+
80
+ window.speechSynthesis.speak(utterance);
81
+ }
82
+ };
83
+
84
+ return (
85
+ <div className="container mx-auto p-4 max-w-2xl">
86
+ <div className="bg-white rounded-lg shadow-md h-[calc(100vh-12rem)] flex flex-col">
87
+ <div className="flex-1 overflow-y-auto p-4">
88
+ {messages.map((message) => (
89
+ <div
90
+ key={message.id}
91
+ className={`flex ${message.isArabic ? 'justify-end' : 'justify-start'} mb-4`}
92
+ >
93
+ <div
94
+ className={`rounded-lg p-3 max-w-[70%] ${
95
+ message.isArabic
96
+ ? 'bg-blue-500 text-white'
97
+ : 'bg-white border border-gray-300'
98
+ }`}
99
+ >
100
+ <p dir={message.isArabic ? 'rtl' : 'ltr'}>{message.text}</p>
101
+ {!message.isArabic && (
102
+ <button
103
+ className="mt-2 p-2 bg-gray-100 rounded-full hover:bg-gray-200 transition-colors"
104
+ onClick={() => speakText(message.text, false)}
105
+ title="Écouter la traduction"
106
+ >
107
+ <Play className="h-4 w-4" />
108
+ </button>
109
+ )}
110
+ </div>
111
+ </div>
112
+ ))}
113
+ </div>
114
+
115
+ <div className="border-t p-4">
116
+ <div className="flex items-center justify-center space-x-4">
117
+ <button
118
+ onClick={isRecording ? stopRecording : startRecording}
119
+ className="relative p-4 rounded-full bg-blue-500 hover:bg-blue-600 text-white"
120
+ >
121
+ {isRecording ? (
122
+ <Square className="h-6 w-6" />
123
+ ) : (
124
+ <Mic className="h-6 w-6" />
125
+ )}
126
+ {isRecording && (
127
+ <div className="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-75"></div>
128
+ )}
129
+ </button>
130
+ <p className="text-gray-600 text-right" dir="rtl">انقر ثم تحدث</p>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ };
137
+
138
+ export default ArabicChat;
front/src/pages/FrenchChat.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import { Mic, Play, Square } from 'lucide-react';
3
+ import { useChat } from '../context/ChatContext';
4
+
5
+ const FrenchChat = () => {
6
+ const { messages, addMessage } = useChat();
7
+ const [isRecording, setIsRecording] = useState(false);
8
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null);
9
+ const chunksRef = useRef<Blob[]>([]);
10
+ const recognitionRef = useRef<SpeechRecognition | null>(null);
11
+
12
+ const startRecording = async () => {
13
+ try {
14
+ // Initialize speech recognition
15
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
16
+ const recognition = new SpeechRecognition();
17
+ recognitionRef.current = recognition;
18
+
19
+ recognition.lang = 'fr-FR';
20
+ recognition.continuous = true;
21
+ recognition.interimResults = true;
22
+
23
+ recognition.onresult = (event) => {
24
+ const transcript = Array.from(event.results)
25
+ .map(result => result[0].transcript)
26
+ .join('');
27
+
28
+ if (event.results[0].isFinal) {
29
+ const newMessage = {
30
+ id: Date.now(),
31
+ text: transcript,
32
+ isFrench: true,
33
+ timestamp: new Date(),
34
+ };
35
+ addMessage(newMessage);
36
+ }
37
+ };
38
+
39
+ recognition.start();
40
+ setIsRecording(true);
41
+
42
+ // Also start audio recording for WAV file
43
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
44
+ const mediaRecorder = new MediaRecorder(stream);
45
+ mediaRecorderRef.current = mediaRecorder;
46
+ chunksRef.current = [];
47
+
48
+ mediaRecorder.ondataavailable = (e) => {
49
+ chunksRef.current.push(e.data);
50
+ };
51
+
52
+ mediaRecorder.start();
53
+ } catch (err) {
54
+ console.error('Error accessing microphone:', err);
55
+ }
56
+ };
57
+
58
+ const stopRecording = () => {
59
+ if (recognitionRef.current) {
60
+ recognitionRef.current.stop();
61
+ }
62
+
63
+ if (mediaRecorderRef.current && isRecording) {
64
+ mediaRecorderRef.current.stop();
65
+ setIsRecording(false);
66
+ mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
67
+ }
68
+ };
69
+
70
+ const speakText = (text: string, isFrench: boolean) => {
71
+ if ('speechSynthesis' in window) {
72
+ window.speechSynthesis.cancel();
73
+
74
+ const utterance = new SpeechSynthesisUtterance(text);
75
+ utterance.lang = isFrench ? 'fr-FR' : 'ar-SA';
76
+ utterance.rate = 1.0;
77
+ utterance.pitch = 1.0;
78
+ utterance.volume = 1.0;
79
+
80
+ window.speechSynthesis.speak(utterance);
81
+ }
82
+ };
83
+
84
+ return (
85
+ <div className="container mx-auto p-4 max-w-2xl">
86
+ <div className="bg-white rounded-lg shadow-md h-[calc(100vh-12rem)] flex flex-col">
87
+ <div className="flex-1 overflow-y-auto p-4">
88
+ {messages.map((message) => (
89
+ <div
90
+ key={message.id}
91
+ className={`flex ${message.isFrench ? 'justify-end' : 'justify-start'} mb-4`}
92
+ >
93
+ <div
94
+ className={`rounded-lg p-3 max-w-[70%] ${
95
+ message.isFrench
96
+ ? 'bg-blue-500 text-white'
97
+ : 'bg-white border border-gray-300'
98
+ }`}
99
+ >
100
+ <p>{message.text}</p>
101
+ {!message.isFrench && (
102
+ <button
103
+ className="mt-2 p-2 bg-gray-100 rounded-full hover:bg-gray-200 transition-colors"
104
+ onClick={() => speakText(message.text, false)}
105
+ title="Écouter le message"
106
+ >
107
+ <Play className="h-4 w-4" />
108
+ </button>
109
+ )}
110
+ </div>
111
+ </div>
112
+ ))}
113
+ </div>
114
+
115
+ <div className="border-t p-4">
116
+ <div className="flex items-center justify-center space-x-4">
117
+ <button
118
+ onClick={isRecording ? stopRecording : startRecording}
119
+ className="relative p-4 rounded-full bg-blue-500 hover:bg-blue-600 text-white"
120
+ >
121
+ {isRecording ? (
122
+ <Square className="h-6 w-6" />
123
+ ) : (
124
+ <Mic className="h-6 w-6" />
125
+ )}
126
+ {isRecording && (
127
+ <div className="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-75"></div>
128
+ )}
129
+ </button>
130
+ <p className="text-gray-600">Cliquer puis parler</p>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ };
137
+
138
+ export default FrenchChat;
front/src/pages/LanguageSelection.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+
4
+ const LanguageSelection = () => {
5
+ const navigate = useNavigate();
6
+
7
+ const languages = [
8
+ {
9
+ name: 'Bulgarian',
10
+ nativeName: 'Говоря български',
11
+ flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
12
+ },
13
+ {
14
+ name: 'Arabic',
15
+ nativeName: 'أتحدث العربية',
16
+ flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
17
+ },
18
+ {
19
+ name: 'Spanish',
20
+ nativeName: 'Hablo español',
21
+ flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
22
+ },
23
+ {
24
+ name: 'Romanian',
25
+ nativeName: 'Vorbesc română',
26
+ flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
27
+ },
28
+ ];
29
+
30
+ return (
31
+ <div className="container mx-auto px-4 py-8">
32
+ <div className="max-w-2xl mx-auto">
33
+ <div className="flex justify-center mb-8">
34
+ <img
35
+ src="https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=200&h=100"
36
+ alt="MSA Logo"
37
+ className="h-24"
38
+ />
39
+ </div>
40
+
41
+ <div className="grid grid-cols-2 gap-4">
42
+ {languages.map((lang) => (
43
+ <button
44
+ key={lang.name}
45
+ onClick={() => navigate('/arabic')}
46
+ className="bg-white rounded-lg shadow-md p-4 flex items-center space-x-4 hover:shadow-lg transition-shadow"
47
+ >
48
+ <img
49
+ src={lang.flag}
50
+ alt={`${lang.name} flag`}
51
+ className="w-12 h-12 rounded-full"
52
+ />
53
+ <div className="text-left">
54
+ <p className="text-sm text-gray-600">Je parle</p>
55
+ <p className="text-lg font-semibold">{lang.nativeName}</p>
56
+ </div>
57
+ </button>
58
+ ))}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export default LanguageSelection;
front/src/vite-env.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface Window {
4
+ SpeechRecognition: typeof SpeechRecognition;
5
+ webkitSpeechRecognition: typeof SpeechRecognition;
6
+ }
front/tailwind.config.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ };
front/tsconfig.app.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["src"]
24
+ }
front/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
front/tsconfig.node.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+
15
+ /* Linting */
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noFallthroughCasesInSwitch": true
20
+ },
21
+ "include": ["vite.config.ts"]
22
+ }
front/vite.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ optimizeDeps: {
8
+ exclude: ['lucide-react'],
9
+ },
10
+ });