From a61674147052d6f1604990f17cf4750a30c877bf Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Tue, 10 Sep 2024 21:30:40 +0200 Subject: [PATCH] Pierwszy commit --- .dockerignore | 3 + .gitignore | 24 +++++ Dockerfile | 20 ++++ README.md | 21 ++++ bun.lockb | Bin 0 -> 106335 bytes docker-compose.yaml | 5 + eslint.config.js | 28 ++++++ index.html | 13 +++ nginx.conf | 8 ++ package.json | 37 +++++++ postcss.config.js | 6 ++ public/garand-ico.svg | 182 +++++++++++++++++++++++++++++++++++ src/App.tsx | 38 ++++++++ src/atoms/authAtom.ts | 15 +++ src/components/Header.tsx | 42 ++++++++ src/components/Post.tsx | 103 ++++++++++++++++++++ src/index.css | 3 + src/lib/pocketbase.ts | 7 ++ src/main.tsx | 10 ++ src/pages/CreatePost.tsx | 60 ++++++++++++ src/pages/ForgotPassword.tsx | 43 +++++++++ src/pages/Home.tsx | 69 +++++++++++++ src/pages/Login.tsx | 62 ++++++++++++ src/pages/Profile.tsx | 99 +++++++++++++++++++ src/pages/Register.tsx | 97 +++++++++++++++++++ src/vite-env.d.ts | 1 + tailwind.config.js | 11 +++ tsconfig.app.json | 24 +++++ tsconfig.json | 7 ++ tsconfig.node.json | 22 +++++ vite.config.ts | 10 ++ 31 files changed, 1070 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 docker-compose.yaml create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 nginx.conf create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/garand-ico.svg create mode 100644 src/App.tsx create mode 100644 src/atoms/authAtom.ts create mode 100644 src/components/Header.tsx create mode 100644 src/components/Post.tsx create mode 100644 src/index.css create mode 100644 src/lib/pocketbase.ts create mode 100644 src/main.tsx create mode 100644 src/pages/CreatePost.tsx create mode 100644 src/pages/ForgotPassword.tsx create mode 100644 src/pages/Home.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/Profile.tsx create mode 100644 src/pages/Register.tsx create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2a20c49 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.vscode/* +node_modules +dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a17a986 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM oven/bun:latest as build + +WORKDIR /app + +COPY package.json bun.lockb ./ +RUN bun install + +COPY . . +RUN bun run build + +# Production stage +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0353b38 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Reddit-Like-App + +Reddit-Like-App to aplikacja webowa inspirowana Redditem, stworzona przy użyciu React, TypeScript oraz szeregu nowoczesnych narzędzi takich jak Vite, Bun, TailwindCSS, Jotai, Docker, Nginx oraz PocketBase (BaaS). Użytkownicy mogą rejestrować konta, dodawać posty oraz głosować na nie w czasie rzeczywistym. Posty są dynamicznie sortowane według liczby głosów, a zmiany są automatycznie aktualizowane dzięki funkcjom real-time PocketBase. + +## Technologie +- **Frontend**: React, Vite, TypeScript, TailwindCSS, PostCSS, Jotai +- **Backend**: PocketBase (Backend as a Service) +- **Serwowanie aplikacji**: Nginx, Docker +- **Inne**: Obsługa realtime za pomocą PocketBase + +## Funkcje +- Tworzenie kont użytkowników +- Dodawanie postów +- Głosowanie na posty (dodawanie/odejmowanie punktów) +- Dynamiczne sortowanie postów według liczby głosów +- Aktualizacje w czasie rzeczywistym dzięki PocketBase + +## Wymagania +- Bun +- Docker (jeśli chcesz uruchomić aplikację w kontenerze) +- PocketBase (BaaS) - uruchomiony lokalnie lub zdalnie diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..325e23929ecdb75f5d333b7f668edda469b0c194 GIT binary patch literal 106335 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 + + + + + + Reddit-Like-App + + +
+ + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..333bc49 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,8 @@ +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b5c28de --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "reddit-like-app", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build-tsc": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@types/react-router-dom": "^5.3.3", + "autoprefixer": "^10.4.20", + "jotai": "^2.9.3", + "pocketbase": "^0.21.5", + "postcss": "^8.4.45", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "tailwindcss": "^3.4.10" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "typescript": "^5.6.2", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/garand-ico.svg b/public/garand-ico.svg new file mode 100644 index 0000000..aa57bc5 --- /dev/null +++ b/public/garand-ico.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..2c5021b --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import { Provider } from 'jotai'; +import Header from './components/Header'; +import Home from './pages/Home'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import ForgotPassword from './pages/ForgotPassword'; +import Profile from './pages/Profile'; +import CreatePost from './pages/CreatePost'; + +const App: React.FC = () => { + return ( + + +
+
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+
+ ); +}; + +export default App; + +// Rdzenny komponent aplikacji. Tutaj używany jest nagłowek +// oraz definiowane są adresy url oraz +// jaki komponent się pod nimi znajduje. \ No newline at end of file diff --git a/src/atoms/authAtom.ts b/src/atoms/authAtom.ts new file mode 100644 index 0000000..6365960 --- /dev/null +++ b/src/atoms/authAtom.ts @@ -0,0 +1,15 @@ +import { atom } from 'jotai'; +import { pb } from '../lib/pocketbase'; + +export const authAtom = atom(pb.authStore.isValid); + +export const updateAuthAtom = atom( + (get) => get(authAtom), + (_, set) => { + set(authAtom, pb.authStore.isValid); + } +); + +// Utworzenie store przy użyciu Jotai +// do przechowywania danych o zalogowanym +// użytkowniku między komponentami. diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..27f017c --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useAtom } from 'jotai'; +import { authAtom, updateAuthAtom } from '../atoms/authAtom'; +import { pb } from '../lib/pocketbase'; + +const Header: React.FC = () => { + const [isAuth] = useAtom(authAtom); + const [, updateAuth] = useAtom(updateAuthAtom); + + const handleLogout = () => { + pb.authStore.clear(); + updateAuth(); + }; + + return ( +
+
+ Reddit-Like-App + Kod źródłowy + +
+
+ ); +}; + +export default Header; + +// komponent odpowiedzialny za nagłówek strony diff --git a/src/components/Post.tsx b/src/components/Post.tsx new file mode 100644 index 0000000..b6b10de --- /dev/null +++ b/src/components/Post.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect } from 'react'; +import { useAtom } from 'jotai'; +import { authAtom } from '../atoms/authAtom'; +import { pb } from '../lib/pocketbase'; + +interface PostProps { + post: { + id: string; + title: string; + content: string; + user: string; + votes: number; + created: string; + }; + onVoteChange?: (postId: string, newVotes: number) => void; +} + +const Post: React.FC = ({ post, onVoteChange }) => { + const [isAuth] = useAtom(authAtom); + const [username, setUsername] = useState(''); + const [votes, setVotes] = useState(post.votes); + + useEffect(() => { + const fetchUsername = async () => { + try { + const user = await pb.collection("users").getOne(post.user); + setUsername(user.username); + } catch (error) { + console.error('Błąd przy pobieraniu nazwy użytkownika:', error); + setUsername('Nieznany użytkownik'); + } + }; + fetchUsername(); + }, [post.user]); + + useEffect(() => { + const subscribeToPost = async () => { + try { + pb.realtime.subscribe(`posts/${post.id}`, (e) => { + if (e.action === 'update' && e.record.id === post.id) { + setVotes(e.record.votes); + } + }); + } catch (error) { + console.error('Błąd przy renderowaniu głosów in real time:', error); + } + }; + + subscribeToPost(); + + return () => { + pb.realtime.unsubscribe(`posts/${post.id}`); + }; + }, [post.id]); + + const handleVote = async (type: 'up' | 'down') => { + if (!isAuth) { + alert('Musisz się zalogować, by móc głosować.'); + return; + } + + const newVotes = type === 'up' ? votes + 1 : votes - 1; + setVotes(newVotes); + + try { + await pb.collection('posts').update(post.id, { votes: newVotes }); + if (onVoteChange) { + onVoteChange(post.id, newVotes); + } + } catch (error) { + console.error('Błąd przy głosowaniu:', error); + setVotes(votes); + alert('Nie udało się dodać głosu. Spróbuj jeszcze raz.'); + } + }; + + return ( +
+
+ + + {votes} + + + + | + + + {username} + + + | + + {new Date(post.created).toLocaleString()} +
+

{post.title}

+

{post.content}

+
+)}; + +export default Post; + +// Komponent odpowiedzialny za renderowanie pojedynczego posta. diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/lib/pocketbase.ts b/src/lib/pocketbase.ts new file mode 100644 index 0000000..33a3731 --- /dev/null +++ b/src/lib/pocketbase.ts @@ -0,0 +1,7 @@ +import PocketBase from 'pocketbase'; + +export const pb = new PocketBase('https://pb.garandplg.com').autoCancellation(false); + +// Inicjacja połączenia z moją instancją pocketbase +// oraz wyeksportowanie tego połączenia +// by było łatwo dostępne w projekcie. diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..6f4ac9b --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/pages/CreatePost.tsx b/src/pages/CreatePost.tsx new file mode 100644 index 0000000..e221911 --- /dev/null +++ b/src/pages/CreatePost.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { pb } from '../lib/pocketbase'; + +const CreatePost: React.FC = () => { + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const navigate = useNavigate(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await pb.collection('posts').create({ + title, + content, + user: pb.authStore.model!.id, + votes: 0, + }); + navigate('/'); + } catch (error) { + console.error('Błąd przy tworzeniu posta:', error); + alert('Nie udało się stworzyć posta'); + } + }; + + return ( +
+

Stwórz post

+
+
+ + setTitle(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+
+ + +
+ +
+
+ ); +}; + +export default CreatePost; + +// Formularz dodawania posta. diff --git a/src/pages/ForgotPassword.tsx b/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..dd5b767 --- /dev/null +++ b/src/pages/ForgotPassword.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import { pb } from '../lib/pocketbase'; + +const ForgotPassword: React.FC = () => { + const [email, setEmail] = useState(''); + const [message, setMessage] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await pb.collection('users').requestPasswordReset(email); + setMessage('Mail z resetem hasła został wysłany.'); + } catch (error) { + console.error('Błąd z resetem hasła:', error); + setMessage('Nie udało się wysłać maila z resetem hasła.'); + } + }; + + return ( +
+

Zapomniałem hasła

+
+
+ + setEmail(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+ +
+ {message &&

{message}

} +
+ ); +}; + +export default ForgotPassword; + +// Formularz przypominania hasła. diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..a68e2e9 --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from 'react'; +import { pb } from '../lib/pocketbase'; +import Post from '../components/Post'; + +interface PostType { + id: string; + title: string; + content: string; + user: string; + votes: number; + created: string; +} + +const Home: React.FC = () => { + const [posts, setPosts] = useState([]); + + useEffect(() => { + const fetchPosts = async () => { + try { + const records = await pb.collection('posts').getFullList({ + sort: '-votes', + expand: 'user', + }); + setPosts(records); + } catch (error) { + console.error('Błąd przy pobieraniu postów:', error); + } + }; + + fetchPosts(); + + pb.realtime.subscribe('posts', (e) => { + const updatedRecord = e.record as PostType; + + if (e.action === 'create') { + setPosts((prevPosts) => [updatedRecord, ...prevPosts].sort((a, b) => b.votes - a.votes)); + } else if (e.action === 'update') { + setPosts((prevPosts) => + [...prevPosts.map((post) => + post.id === updatedRecord.id ? updatedRecord : post + )].sort((a, b) => b.votes - a.votes) + ); + } else if (e.action === 'delete') { + setPosts((prevPosts) => + prevPosts.filter((post) => post.id !== updatedRecord.id) + ); + } + }); + + return () => { + pb.realtime.unsubscribe(); + }; + }, []); + + return ( +
+

Najwyżej oceniane posty

+
+ {posts.map((post) => ( + + ))} +
+
+ ); +}; + +export default Home; + +// Strona głowna, tutaj wyświetlają się posty. diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..2a846ab --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { pb } from '../lib/pocketbase'; +import { useAtom } from 'jotai'; +import { updateAuthAtom } from '../atoms/authAtom'; + +const Login: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [, updateAuth] = useAtom(updateAuthAtom); + const navigate = useNavigate(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await pb.collection('users').authWithPassword(email, password); + updateAuth(); + navigate('/'); + } catch (error) { + console.error('Błąd logowania:', error); + alert('Nie udało się zalogować'); + } + }; + + return ( +
+

Login

+
+
+ + setEmail(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setPassword(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+ +
+
+ Zapomniałeś hasła? +
+
+ ); +}; + +export default Login; + +// Formularz logowania się. diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx new file mode 100644 index 0000000..757566e --- /dev/null +++ b/src/pages/Profile.tsx @@ -0,0 +1,99 @@ +import React, { useState, useEffect } from 'react'; +import { pb } from '../lib/pocketbase'; +import { useAtom } from 'jotai'; +import { updateAuthAtom } from '../atoms/authAtom'; + +const Profile: React.FC = () => { + const [email, setEmail] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [, updateAuth] = useAtom(updateAuthAtom); + + useEffect(() => { + if (pb.authStore.model) { + setEmail(pb.authStore.model.email); + } + }, []); + + const handleUpdateProfile = async (e: React.FormEvent) => { + e.preventDefault(); + if (newPassword && newPassword !== confirmPassword) { + alert("Hasła nie są takie same"); + return; + } + + try { + const data: Record = { email }; + if (newPassword) { + data.password = newPassword; + data.passwordConfirm = confirmPassword; + } + + await pb.collection('users').update(pb.authStore.model!.id, data); + alert('Profile updated successfully'); + updateAuth(); + } catch (error) { + console.error('Profile update error:', error); + alert('Failed to update profile'); + } + }; + + const handleDeleteAccount = async () => { + if (window.confirm('Jesteś pewien, że chcesz usunąć konto? Tej akcji nie da się cofnąć.')) { + try { + await pb.collection('users').delete(pb.authStore.model!.id); + pb.authStore.clear(); + updateAuth(); + alert('Konto poprawnie usunięte'); + } catch (error) { + console.error('Błąd z usuwaniem konta:', error); + alert('Nie udało się usunąc konta.'); + } + } + }; + + return ( +
+

Profile

+
+
+ + setEmail(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setNewPassword(e.target.value)} + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setConfirmPassword(e.target.value)} + className="w-full px-3 py-2 border rounded" + /> +
+ +
+ +
+ ); +}; + +export default Profile; + +// Formularz zmieniania profilu użytkownika diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx new file mode 100644 index 0000000..3533f51 --- /dev/null +++ b/src/pages/Register.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { pb } from '../lib/pocketbase'; +import { useAtom } from 'jotai'; +import { updateAuthAtom } from '../atoms/authAtom'; + +const Register: React.FC = () => { + const [email, setEmail] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirm, setPasswordConfirm] = useState(''); + const [, updateAuth] = useAtom(updateAuthAtom); + const navigate = useNavigate(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== passwordConfirm) { + alert("Hasła nie są takie same!"); + return; + } + try { + const createdUser = await pb.collection('users').create({ + email, + username, + password, + passwordConfirm, + }); + + if (createdUser) { + await pb.collection('users').requestVerification(email); + alert('Udana rejestracja. Sprawdź swój adres email aby aktywować konto.'); + } + + navigate('/login'); + } catch (error) { + console.error('Błąd rejestracji:', error); + alert('Nieudana rejestracja'); + } + }; + + return ( +
+

Zarejestruj się

+
+
+ + setEmail(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setUsername(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setPassword(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+
+ + setPasswordConfirm(e.target.value)} + required + className="w-full px-3 py-2 border rounded" + /> +
+ +
+
+ ); +}; + +export default Register; + +// Formularz rejestracji. diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..89a305e --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..f0a2350 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..0d3d714 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0dd4c08 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + } +})