cleanups
This commit is contained in:
parent
06162e4f66
commit
3d3ee3a087
13 changed files with 332 additions and 238 deletions
|
@ -26,4 +26,4 @@
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
src/App.jsx
20
src/App.jsx
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import Header from './components/Header';
|
import Header from "./components/Header";
|
||||||
import Home from './pages/Home';
|
import Home from "./pages/Home";
|
||||||
import Login from './pages/Login';
|
import Login from "./pages/Login";
|
||||||
import Mikroblog from './pages/Mikroblog';
|
import Mikroblog from "./pages/Mikroblog";
|
||||||
import Profile from './pages/Profile';
|
import Profile from "./pages/Profile";
|
||||||
import AdminPanel from './pages/AdminPanel';
|
import AdminPanel from "./pages/AdminPanel";
|
||||||
// import Settings from './pages/Settings';
|
// import Settings from './pages/Settings';
|
||||||
import './styles/main.scss';
|
import "./styles/main.scss";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
@ -29,4 +29,4 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { LogIn, LogOut, Home, MessageSquare, Settings, Shield } from 'lucide-react';
|
import {
|
||||||
import { useStore } from '../store/useStore';
|
LogIn,
|
||||||
|
LogOut,
|
||||||
|
Home,
|
||||||
|
MessageSquare,
|
||||||
|
Settings,
|
||||||
|
Shield,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useStore } from "../store/useStore";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const { publicKey, profile, logout } = useStore();
|
const { publicKey, profile, logout } = useStore();
|
||||||
|
@ -20,11 +27,11 @@ function Header() {
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/mikroblog">Mikroblog</Link>
|
<Link to="/mikroblog">Mikroblog</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="header__nav">
|
<div className="header__nav">
|
||||||
{publicKey ? (
|
{publicKey ? (
|
||||||
<>
|
<>
|
||||||
{(profile?.role === 'admin' || profile?.role === 'moderator') && (
|
{(profile?.role === "admin" || profile?.role === "moderator") && (
|
||||||
<Link to="/admin">
|
<Link to="/admin">
|
||||||
<Shield size={16} />
|
<Shield size={16} />
|
||||||
Panel
|
Panel
|
||||||
|
@ -33,7 +40,7 @@ function Header() {
|
||||||
<Link to="/settings">
|
<Link to="/settings">
|
||||||
<Settings size={20} />
|
<Settings size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
<button onClick={logout} className="button">
|
<button type="button" onClick={logout} className="button">
|
||||||
<LogOut size={20} />
|
<LogOut size={20} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
@ -49,4 +56,4 @@ function Header() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|
12
src/main.jsx
12
src/main.jsx
|
@ -1,9 +1,9 @@
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from "react-dom/client";
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,23 +1,40 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Shield, User, MessageSquare, Ban, CheckCircle, XCircle } from 'lucide-react';
|
import {
|
||||||
import { useStore } from '../store/useStore';
|
Shield,
|
||||||
import { fetchAllPosts, fetchAllUsers, updateUserRole, banUser, removePost } from '../utils/nostr';
|
User,
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
MessageSquare,
|
||||||
import { pl } from 'date-fns/locale';
|
Ban,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useStore } from "../store/useStore";
|
||||||
|
import {
|
||||||
|
fetchAllPosts,
|
||||||
|
fetchAllUsers,
|
||||||
|
updateUserRole,
|
||||||
|
banUser,
|
||||||
|
removePost,
|
||||||
|
} from "../utils/nostr";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { pl } from "date-fns/locale";
|
||||||
|
|
||||||
function AdminPanel() {
|
function AdminPanel() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { publicKey, profile } = useStore();
|
const { publicKey, profile } = useStore();
|
||||||
const [activeTab, setActiveTab] = useState('users');
|
const [activeTab, setActiveTab] = useState("users");
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [processingAction, setProcessingAction] = useState({});
|
const [processingAction, setProcessingAction] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!publicKey || !profile || (profile.role !== 'admin' && profile.role !== 'moderator')) {
|
if (
|
||||||
navigate('/');
|
!publicKey ||
|
||||||
|
!profile ||
|
||||||
|
(profile.role !== "admin" && profile.role !== "moderator")
|
||||||
|
) {
|
||||||
|
navigate("/");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,12 +46,12 @@ function AdminPanel() {
|
||||||
try {
|
try {
|
||||||
const [fetchedUsers, fetchedPosts] = await Promise.all([
|
const [fetchedUsers, fetchedPosts] = await Promise.all([
|
||||||
fetchAllUsers(),
|
fetchAllUsers(),
|
||||||
fetchAllPosts()
|
fetchAllPosts(),
|
||||||
]);
|
]);
|
||||||
setUsers(fetchedUsers);
|
setUsers(fetchedUsers);
|
||||||
setPosts(fetchedPosts);
|
setPosts(fetchedPosts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading admin data:', error);
|
console.error("Error loading admin data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -43,42 +60,42 @@ function AdminPanel() {
|
||||||
async function handleUpdateRole(userId, newRole) {
|
async function handleUpdateRole(userId, newRole) {
|
||||||
if (processingAction[userId]) return;
|
if (processingAction[userId]) return;
|
||||||
|
|
||||||
setProcessingAction(prev => ({ ...prev, [userId]: true }));
|
setProcessingAction((prev) => ({ ...prev, [userId]: true }));
|
||||||
try {
|
try {
|
||||||
await updateUserRole(userId, newRole);
|
await updateUserRole(userId, newRole);
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user role:', error);
|
console.error("Error updating user role:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessingAction(prev => ({ ...prev, [userId]: false }));
|
setProcessingAction((prev) => ({ ...prev, [userId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBanUser(userId, isBanned) {
|
async function handleBanUser(userId, isBanned) {
|
||||||
if (processingAction[userId]) return;
|
if (processingAction[userId]) return;
|
||||||
|
|
||||||
setProcessingAction(prev => ({ ...prev, [userId]: true }));
|
setProcessingAction((prev) => ({ ...prev, [userId]: true }));
|
||||||
try {
|
try {
|
||||||
await banUser(userId, isBanned);
|
await banUser(userId, isBanned);
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user ban status:', error);
|
console.error("Error updating user ban status:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessingAction(prev => ({ ...prev, [userId]: false }));
|
setProcessingAction((prev) => ({ ...prev, [userId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemovePost(postId) {
|
async function handleRemovePost(postId) {
|
||||||
if (processingAction[postId]) return;
|
if (processingAction[postId]) return;
|
||||||
|
|
||||||
setProcessingAction(prev => ({ ...prev, [postId]: true }));
|
setProcessingAction((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await removePost(postId);
|
await removePost(postId);
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error removing post:', error);
|
console.error("Error removing post:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessingAction(prev => ({ ...prev, [postId]: false }));
|
setProcessingAction((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,28 +109,30 @@ function AdminPanel() {
|
||||||
<div className="admin-panel__header">
|
<div className="admin-panel__header">
|
||||||
<h1>
|
<h1>
|
||||||
<Shield size={24} />
|
<Shield size={24} />
|
||||||
Panel {profile?.role === 'admin' ? 'Administratora' : 'Moderatora'}
|
Panel {profile?.role === "admin" ? "Administratora" : "Moderatora"}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<button
|
<button
|
||||||
className={`tab ${activeTab === 'users' ? 'tab--active' : ''}`}
|
type="button"
|
||||||
onClick={() => setActiveTab('users')}
|
className={`tab ${activeTab === "users" ? "tab--active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("users")}
|
||||||
>
|
>
|
||||||
<User size={16} />
|
<User size={16} />
|
||||||
Użytkownicy
|
Użytkownicy
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab ${activeTab === 'posts' ? 'tab--active' : ''}`}
|
type="button"
|
||||||
onClick={() => setActiveTab('posts')}
|
className={`tab ${activeTab === "posts" ? "tab--active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("posts")}
|
||||||
>
|
>
|
||||||
<MessageSquare size={16} />
|
<MessageSquare size={16} />
|
||||||
Wpisy
|
Wpisy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'users' ? (
|
{activeTab === "users" ? (
|
||||||
<div className="admin-panel__users">
|
<div className="admin-panel__users">
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -125,8 +144,11 @@ function AdminPanel() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{users.map(user => (
|
{users.map((user) => (
|
||||||
<tr key={user.id} className={user.banned ? 'table__row--banned' : ''}>
|
<tr
|
||||||
|
key={user.id}
|
||||||
|
className={user.banned ? "table__row--banned" : ""}
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<div className="table__user">
|
<div className="table__user">
|
||||||
{user.picture ? (
|
{user.picture ? (
|
||||||
|
@ -140,7 +162,7 @@ function AdminPanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{user.role || 'user'}</td>
|
<td>{user.role || "user"}</td>
|
||||||
<td>
|
<td>
|
||||||
{user.banned ? (
|
{user.banned ? (
|
||||||
<span className="badge badge--danger">
|
<span className="badge badge--danger">
|
||||||
|
@ -156,10 +178,12 @@ function AdminPanel() {
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="table__actions">
|
<div className="table__actions">
|
||||||
{profile?.role === 'admin' && (
|
{profile?.role === "admin" && (
|
||||||
<select
|
<select
|
||||||
value={user.role || 'user'}
|
value={user.role || "user"}
|
||||||
onChange={(e) => handleUpdateRole(user.id, e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleUpdateRole(user.id, e.target.value)
|
||||||
|
}
|
||||||
disabled={processingAction[user.id]}
|
disabled={processingAction[user.id]}
|
||||||
>
|
>
|
||||||
<option value="user">Użytkownik</option>
|
<option value="user">Użytkownik</option>
|
||||||
|
@ -168,7 +192,8 @@ function AdminPanel() {
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className={`button ${user.banned ? 'button--success' : 'button--danger'}`}
|
type="button"
|
||||||
|
className={`button ${user.banned ? "button--success" : "button--danger"}`}
|
||||||
onClick={() => handleBanUser(user.id, !user.banned)}
|
onClick={() => handleBanUser(user.id, !user.banned)}
|
||||||
disabled={processingAction[user.id]}
|
disabled={processingAction[user.id]}
|
||||||
>
|
>
|
||||||
|
@ -203,17 +228,22 @@ function AdminPanel() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{posts.map(post => (
|
{posts.map((post) => (
|
||||||
<tr key={post.id}>
|
<tr key={post.id}>
|
||||||
<td>
|
<td>
|
||||||
<div className="table__user">
|
<div className="table__user">
|
||||||
{post.author.picture ? (
|
{post.author.picture ? (
|
||||||
<img src={post.author.picture} alt={post.author.name} />
|
<img
|
||||||
|
src={post.author.picture}
|
||||||
|
alt={post.author.name}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<User size={24} />
|
<User size={24} />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<strong>{post.author.name || post.author.id.slice(0, 8)}</strong>
|
<strong>
|
||||||
|
{post.author.name || post.author.id.slice(0, 8)}
|
||||||
|
</strong>
|
||||||
<small>{post.author.id}</small>
|
<small>{post.author.id}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -222,24 +252,21 @@ function AdminPanel() {
|
||||||
<div className="table__content">
|
<div className="table__content">
|
||||||
<p>{post.content}</p>
|
<p>{post.content}</p>
|
||||||
<div className="table__meta">
|
<div className="table__meta">
|
||||||
<span>
|
<span>{post.votes.up - post.votes.down} punktów</span>
|
||||||
{post.votes.up - post.votes.down} punktów
|
|
||||||
</span>
|
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>
|
<span>{post.comments} komentarzy</span>
|
||||||
{post.comments} komentarzy
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{formatDistanceToNow(post.createdAt * 1000, {
|
{formatDistanceToNow(post.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="button button--danger"
|
className="button button--danger"
|
||||||
onClick={() => handleRemovePost(post.id)}
|
onClick={() => handleRemovePost(post.id)}
|
||||||
disabled={processingAction[post.id]}
|
disabled={processingAction[post.id]}
|
||||||
|
@ -259,4 +286,4 @@ function AdminPanel() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminPanel;
|
export default AdminPanel;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from 'lucide-react';
|
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from "lucide-react";
|
||||||
import { useStore } from '../store/useStore';
|
import { useStore } from "../store/useStore";
|
||||||
import { fetchPosts, publishPost, vote, fetchComments } from '../utils/nostr';
|
import { fetchPosts, publishPost, vote, fetchComments } from "../utils/nostr";
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { pl } from 'date-fns/locale';
|
import { pl } from "date-fns/locale";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [newPost, setNewPost] = useState('');
|
const [newPost, setNewPost] = useState("");
|
||||||
const [publishing, setPublishing] = useState(false);
|
const [publishing, setPublishing] = useState(false);
|
||||||
const [votingStates, setVotingStates] = useState({});
|
const [votingStates, setVotingStates] = useState({});
|
||||||
const [expandedComments, setExpandedComments] = useState({});
|
const [expandedComments, setExpandedComments] = useState({});
|
||||||
|
@ -26,7 +26,7 @@ function Home() {
|
||||||
const fetchedPosts = await fetchPosts();
|
const fetchedPosts = await fetchPosts();
|
||||||
setPosts(fetchedPosts.sort((a, b) => b.createdAt - a.createdAt));
|
setPosts(fetchedPosts.sort((a, b) => b.createdAt - a.createdAt));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading posts:', error);
|
console.error("Error loading posts:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,10 @@ function Home() {
|
||||||
setPublishing(true);
|
setPublishing(true);
|
||||||
try {
|
try {
|
||||||
await publishPost(newPost, privateKey);
|
await publishPost(newPost, privateKey);
|
||||||
setNewPost('');
|
setNewPost("");
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing post:', error);
|
console.error("Error publishing post:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPublishing(false);
|
setPublishing(false);
|
||||||
}
|
}
|
||||||
|
@ -51,14 +51,14 @@ function Home() {
|
||||||
async function handleVote(postId, postAuthor, isUpvote) {
|
async function handleVote(postId, postAuthor, isUpvote) {
|
||||||
if (!privateKey || votingStates[postId]) return;
|
if (!privateKey || votingStates[postId]) return;
|
||||||
|
|
||||||
setVotingStates(prev => ({ ...prev, [postId]: true }));
|
setVotingStates((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await vote(postId, postAuthor, isUpvote, privateKey);
|
await vote(postId, postAuthor, isUpvote, privateKey);
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error voting:', error);
|
console.error("Error voting:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setVotingStates(prev => ({ ...prev, [postId]: false }));
|
setVotingStates((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,30 +66,30 @@ function Home() {
|
||||||
if (!expandedComments[postId]) {
|
if (!expandedComments[postId]) {
|
||||||
try {
|
try {
|
||||||
const fetchedComments = await fetchComments(postId);
|
const fetchedComments = await fetchComments(postId);
|
||||||
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
|
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
|
||||||
setExpandedComments(prev => ({ ...prev, [postId]: true }));
|
setExpandedComments((prev) => ({ ...prev, [postId]: true }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching comments:', error);
|
console.error("Error fetching comments:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setExpandedComments(prev => ({ ...prev, [postId]: false }));
|
setExpandedComments((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePublishComment(postId) {
|
async function handlePublishComment(postId) {
|
||||||
if (!newComments[postId]?.trim() || !privateKey) return;
|
if (!newComments[postId]?.trim() || !privateKey) return;
|
||||||
|
|
||||||
setPublishingComments(prev => ({ ...prev, [postId]: true }));
|
setPublishingComments((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await publishPost(newComments[postId], privateKey, postId);
|
await publishPost(newComments[postId], privateKey, postId);
|
||||||
setNewComments(prev => ({ ...prev, [postId]: '' }));
|
setNewComments((prev) => ({ ...prev, [postId]: "" }));
|
||||||
const fetchedComments = await fetchComments(postId);
|
const fetchedComments = await fetchComments(postId);
|
||||||
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
|
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing comment:', error);
|
console.error("Error publishing comment:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPublishingComments(prev => ({ ...prev, [postId]: false }));
|
setPublishingComments((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ function Home() {
|
||||||
className="button"
|
className="button"
|
||||||
>
|
>
|
||||||
<Send size={20} />
|
<Send size={20} />
|
||||||
{publishing ? 'Publikowanie...' : 'Opublikuj'}
|
{publishing ? "Publikowanie..." : "Opublikuj"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -130,7 +130,8 @@ function Home() {
|
||||||
<div key={post.id} className="post">
|
<div key={post.id} className="post">
|
||||||
<div className="post__votes">
|
<div className="post__votes">
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[post.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
|
className={`button ${!publicKey || votingStates[post.id] ? "button--disabled" : ""}`}
|
||||||
onClick={() => handleVote(post.id, post.author, true)}
|
onClick={() => handleVote(post.id, post.author, true)}
|
||||||
disabled={!publicKey || votingStates[post.id]}
|
disabled={!publicKey || votingStates[post.id]}
|
||||||
>
|
>
|
||||||
|
@ -138,7 +139,8 @@ function Home() {
|
||||||
</button>
|
</button>
|
||||||
<span>{post.votes.up - post.votes.down}</span>
|
<span>{post.votes.up - post.votes.down}</span>
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[post.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
|
className={`button ${!publicKey || votingStates[post.id] ? "button--disabled" : ""}`}
|
||||||
onClick={() => handleVote(post.id, post.author, false)}
|
onClick={() => handleVote(post.id, post.author, false)}
|
||||||
disabled={!publicKey || votingStates[post.id]}
|
disabled={!publicKey || votingStates[post.id]}
|
||||||
>
|
>
|
||||||
|
@ -153,11 +155,12 @@ function Home() {
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNow(post.createdAt * 1000, {
|
{formatDistanceToNow(post.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleExpandComments(post.id)}
|
onClick={() => handleExpandComments(post.id)}
|
||||||
className="button button--link"
|
className="button button--link"
|
||||||
>
|
>
|
||||||
|
@ -171,40 +174,54 @@ function Home() {
|
||||||
{publicKey && (
|
{publicKey && (
|
||||||
<div className="comments__form">
|
<div className="comments__form">
|
||||||
<textarea
|
<textarea
|
||||||
value={newComments[post.id] || ''}
|
value={newComments[post.id] || ""}
|
||||||
onChange={(e) => setNewComments(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setNewComments((prev) => ({
|
||||||
[post.id]: e.target.value
|
...prev,
|
||||||
}))}
|
[post.id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
placeholder="Napisz komentarz..."
|
placeholder="Napisz komentarz..."
|
||||||
className="post-input"
|
className="post-input"
|
||||||
rows="2"
|
rows="2"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handlePublishComment(post.id)}
|
onClick={() => handlePublishComment(post.id)}
|
||||||
disabled={publishingComments[post.id] || !newComments[post.id]?.trim()}
|
disabled={
|
||||||
|
publishingComments[post.id] ||
|
||||||
|
!newComments[post.id]?.trim()
|
||||||
|
}
|
||||||
className="button"
|
className="button"
|
||||||
>
|
>
|
||||||
<Reply size={16} />
|
<Reply size={16} />
|
||||||
{publishingComments[post.id] ? 'Wysyłanie...' : 'Odpowiedz'}
|
{publishingComments[post.id]
|
||||||
|
? "Wysyłanie..."
|
||||||
|
: "Odpowiedz"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{comments[post.id]?.map(comment => (
|
{comments[post.id]?.map((comment) => (
|
||||||
<div key={comment.id} className="comment">
|
<div key={comment.id} className="comment">
|
||||||
<div className="comment__votes">
|
<div className="comment__votes">
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
onClick={() => handleVote(comment.id, comment.author, true)}
|
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVote(comment.id, comment.author, true)
|
||||||
|
}
|
||||||
disabled={!publicKey || votingStates[comment.id]}
|
disabled={!publicKey || votingStates[comment.id]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={16} />
|
<ArrowUp size={16} />
|
||||||
</button>
|
</button>
|
||||||
<span>{comment.votes.up - comment.votes.down}</span>
|
<span>{comment.votes.up - comment.votes.down}</span>
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
onClick={() => handleVote(comment.id, comment.author, false)}
|
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVote(comment.id, comment.author, false)
|
||||||
|
}
|
||||||
disabled={!publicKey || votingStates[comment.id]}
|
disabled={!publicKey || votingStates[comment.id]}
|
||||||
>
|
>
|
||||||
<ArrowDown size={16} />
|
<ArrowDown size={16} />
|
||||||
|
@ -218,7 +235,7 @@ function Home() {
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNow(comment.createdAt * 1000, {
|
{formatDistanceToNow(comment.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -236,4 +253,4 @@ function Home() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
||||||
import { useStore } from '../store/useStore';
|
import { useStore } from "../store/useStore";
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -14,9 +14,9 @@ function Login() {
|
||||||
const privateKey = generateSecretKey();
|
const privateKey = generateSecretKey();
|
||||||
const publicKey = getPublicKey(privateKey);
|
const publicKey = getPublicKey(privateKey);
|
||||||
setKeys(publicKey, privateKey);
|
setKeys(publicKey, privateKey);
|
||||||
navigate('/');
|
navigate("/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating keys:', error);
|
console.error("Error generating keys:", error);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
@ -26,11 +26,12 @@ function Login() {
|
||||||
<h2>Zaloguj się</h2>
|
<h2>Zaloguj się</h2>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={handleGenerateKeys}
|
onClick={handleGenerateKeys}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="button button--full"
|
className="button button--full"
|
||||||
>
|
>
|
||||||
{loading ? 'Generowanie...' : 'Wygeneruj nowy klucz'}
|
{loading ? "Generowanie..." : "Wygeneruj nowy klucz"}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-light">
|
<p className="text-light">
|
||||||
Twój klucz prywatny zostanie bezpiecznie zapisany w przeglądarce
|
Twój klucz prywatny zostanie bezpiecznie zapisany w przeglądarce
|
||||||
|
@ -40,4 +41,4 @@ function Login() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from 'lucide-react';
|
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from "lucide-react";
|
||||||
import { useStore } from '../store/useStore';
|
import { useStore } from "../store/useStore";
|
||||||
import { fetchPosts, publishPost, vote, fetchComments } from '../utils/nostr';
|
import { fetchPosts, publishPost, vote, fetchComments } from "../utils/nostr";
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { pl } from 'date-fns/locale';
|
import { pl } from "date-fns/locale";
|
||||||
|
|
||||||
function Mikroblog() {
|
function Mikroblog() {
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [newPost, setNewPost] = useState('');
|
const [newPost, setNewPost] = useState("");
|
||||||
const [publishing, setPublishing] = useState(false);
|
const [publishing, setPublishing] = useState(false);
|
||||||
const [votingStates, setVotingStates] = useState({});
|
const [votingStates, setVotingStates] = useState({});
|
||||||
const [expandedComments, setExpandedComments] = useState({});
|
const [expandedComments, setExpandedComments] = useState({});
|
||||||
|
@ -23,10 +23,10 @@ function Mikroblog() {
|
||||||
|
|
||||||
async function loadPosts() {
|
async function loadPosts() {
|
||||||
try {
|
try {
|
||||||
const fetchedPosts = await fetchPosts('mikroblog');
|
const fetchedPosts = await fetchPosts("mikroblog");
|
||||||
setPosts(fetchedPosts.sort((a, b) => b.createdAt - a.createdAt));
|
setPosts(fetchedPosts.sort((a, b) => b.createdAt - a.createdAt));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading posts:', error);
|
console.error("Error loading posts:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -37,17 +37,17 @@ function Mikroblog() {
|
||||||
if (!newPost.trim() || !privateKey) return;
|
if (!newPost.trim() || !privateKey) return;
|
||||||
|
|
||||||
if (newPost.length > 280) {
|
if (newPost.length > 280) {
|
||||||
alert('Wpis nie może być dłuższy niż 280 znaków!');
|
alert("Wpis nie może być dłuższy niż 280 znaków!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPublishing(true);
|
setPublishing(true);
|
||||||
try {
|
try {
|
||||||
await publishPost(newPost, privateKey, null, 'mikroblog');
|
await publishPost(newPost, privateKey, null, "mikroblog");
|
||||||
setNewPost('');
|
setNewPost("");
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing post:', error);
|
console.error("Error publishing post:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPublishing(false);
|
setPublishing(false);
|
||||||
}
|
}
|
||||||
|
@ -56,14 +56,14 @@ function Mikroblog() {
|
||||||
async function handleVote(postId, postAuthor, isUpvote) {
|
async function handleVote(postId, postAuthor, isUpvote) {
|
||||||
if (!privateKey || votingStates[postId]) return;
|
if (!privateKey || votingStates[postId]) return;
|
||||||
|
|
||||||
setVotingStates(prev => ({ ...prev, [postId]: true }));
|
setVotingStates((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await vote(postId, postAuthor, isUpvote, privateKey);
|
await vote(postId, postAuthor, isUpvote, privateKey);
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error voting:', error);
|
console.error("Error voting:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setVotingStates(prev => ({ ...prev, [postId]: false }));
|
setVotingStates((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,13 +71,13 @@ function Mikroblog() {
|
||||||
if (!expandedComments[postId]) {
|
if (!expandedComments[postId]) {
|
||||||
try {
|
try {
|
||||||
const fetchedComments = await fetchComments(postId);
|
const fetchedComments = await fetchComments(postId);
|
||||||
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
|
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
|
||||||
setExpandedComments(prev => ({ ...prev, [postId]: true }));
|
setExpandedComments((prev) => ({ ...prev, [postId]: true }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching comments:', error);
|
console.error("Error fetching comments:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setExpandedComments(prev => ({ ...prev, [postId]: false }));
|
setExpandedComments((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,21 +85,21 @@ function Mikroblog() {
|
||||||
if (!newComments[postId]?.trim() || !privateKey) return;
|
if (!newComments[postId]?.trim() || !privateKey) return;
|
||||||
|
|
||||||
if (newComments[postId].length > 280) {
|
if (newComments[postId].length > 280) {
|
||||||
alert('Komentarz nie może być dłuższy niż 280 znaków!');
|
alert("Komentarz nie może być dłuższy niż 280 znaków!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPublishingComments(prev => ({ ...prev, [postId]: true }));
|
setPublishingComments((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await publishPost(newComments[postId], privateKey, postId, 'mikroblog');
|
await publishPost(newComments[postId], privateKey, postId, "mikroblog");
|
||||||
setNewComments(prev => ({ ...prev, [postId]: '' }));
|
setNewComments((prev) => ({ ...prev, [postId]: "" }));
|
||||||
const fetchedComments = await fetchComments(postId);
|
const fetchedComments = await fetchComments(postId);
|
||||||
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
|
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing comment:', error);
|
console.error("Error publishing comment:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPublishingComments(prev => ({ ...prev, [postId]: false }));
|
setPublishingComments((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +121,7 @@ function Mikroblog() {
|
||||||
rows="3"
|
rows="3"
|
||||||
maxLength={280}
|
maxLength={280}
|
||||||
/>
|
/>
|
||||||
<span className="character-count">
|
<span className="character-count">{newPost.length}/280</span>
|
||||||
{newPost.length}/280
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -131,7 +129,7 @@ function Mikroblog() {
|
||||||
className="button"
|
className="button"
|
||||||
>
|
>
|
||||||
<Send size={20} />
|
<Send size={20} />
|
||||||
{publishing ? 'Publikowanie...' : 'Opublikuj'}
|
{publishing ? "Publikowanie..." : "Opublikuj"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -146,7 +144,8 @@ function Mikroblog() {
|
||||||
<div key={post.id} className="post">
|
<div key={post.id} className="post">
|
||||||
<div className="post__votes">
|
<div className="post__votes">
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[post.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
|
className={`button ${!publicKey || votingStates[post.id] ? "button--disabled" : ""}`}
|
||||||
onClick={() => handleVote(post.id, post.author, true)}
|
onClick={() => handleVote(post.id, post.author, true)}
|
||||||
disabled={!publicKey || votingStates[post.id]}
|
disabled={!publicKey || votingStates[post.id]}
|
||||||
>
|
>
|
||||||
|
@ -154,7 +153,8 @@ function Mikroblog() {
|
||||||
</button>
|
</button>
|
||||||
<span>{post.votes.up - post.votes.down}</span>
|
<span>{post.votes.up - post.votes.down}</span>
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[post.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
|
className={`button ${!publicKey || votingStates[post.id] ? "button--disabled" : ""}`}
|
||||||
onClick={() => handleVote(post.id, post.author, false)}
|
onClick={() => handleVote(post.id, post.author, false)}
|
||||||
disabled={!publicKey || votingStates[post.id]}
|
disabled={!publicKey || votingStates[post.id]}
|
||||||
>
|
>
|
||||||
|
@ -169,11 +169,12 @@ function Mikroblog() {
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNow(post.createdAt * 1000, {
|
{formatDistanceToNow(post.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleExpandComments(post.id)}
|
onClick={() => handleExpandComments(post.id)}
|
||||||
className="button button--link"
|
className="button button--link"
|
||||||
>
|
>
|
||||||
|
@ -188,45 +189,59 @@ function Mikroblog() {
|
||||||
<div className="comments__form">
|
<div className="comments__form">
|
||||||
<div className="post-input-wrapper">
|
<div className="post-input-wrapper">
|
||||||
<textarea
|
<textarea
|
||||||
value={newComments[post.id] || ''}
|
value={newComments[post.id] || ""}
|
||||||
onChange={(e) => setNewComments(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setNewComments((prev) => ({
|
||||||
[post.id]: e.target.value
|
...prev,
|
||||||
}))}
|
[post.id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
placeholder="Napisz komentarz... (max 280 znaków)"
|
placeholder="Napisz komentarz... (max 280 znaków)"
|
||||||
className="post-input"
|
className="post-input"
|
||||||
rows="2"
|
rows="2"
|
||||||
maxLength={280}
|
maxLength={280}
|
||||||
/>
|
/>
|
||||||
<span className="character-count">
|
<span className="character-count">
|
||||||
{(newComments[post.id] || '').length}/280
|
{(newComments[post.id] || "").length}/280
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handlePublishComment(post.id)}
|
onClick={() => handlePublishComment(post.id)}
|
||||||
disabled={publishingComments[post.id] || !newComments[post.id]?.trim()}
|
disabled={
|
||||||
|
publishingComments[post.id] ||
|
||||||
|
!newComments[post.id]?.trim()
|
||||||
|
}
|
||||||
className="button"
|
className="button"
|
||||||
>
|
>
|
||||||
<Reply size={16} />
|
<Reply size={16} />
|
||||||
{publishingComments[post.id] ? 'Wysyłanie...' : 'Odpowiedz'}
|
{publishingComments[post.id]
|
||||||
|
? "Wysyłanie..."
|
||||||
|
: "Odpowiedz"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{comments[post.id]?.map(comment => (
|
{comments[post.id]?.map((comment) => (
|
||||||
<div key={comment.id} className="comment">
|
<div key={comment.id} className="comment">
|
||||||
<div className="comment__votes">
|
<div className="comment__votes">
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
onClick={() => handleVote(comment.id, comment.author, true)}
|
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVote(comment.id, comment.author, true)
|
||||||
|
}
|
||||||
disabled={!publicKey || votingStates[comment.id]}
|
disabled={!publicKey || votingStates[comment.id]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={16} />
|
<ArrowUp size={16} />
|
||||||
</button>
|
</button>
|
||||||
<span>{comment.votes.up - comment.votes.down}</span>
|
<span>{comment.votes.up - comment.votes.down}</span>
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
onClick={() => handleVote(comment.id, comment.author, false)}
|
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVote(comment.id, comment.author, false)
|
||||||
|
}
|
||||||
disabled={!publicKey || votingStates[comment.id]}
|
disabled={!publicKey || votingStates[comment.id]}
|
||||||
>
|
>
|
||||||
<ArrowDown size={16} />
|
<ArrowDown size={16} />
|
||||||
|
@ -240,7 +255,7 @@ function Mikroblog() {
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNow(comment.createdAt * 1000, {
|
{formatDistanceToNow(comment.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,4 +273,4 @@ function Mikroblog() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Mikroblog;
|
export default Mikroblog;
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from "react-router-dom";
|
||||||
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply, User, Calendar } from 'lucide-react';
|
import {
|
||||||
import { useStore } from '../store/useStore';
|
ArrowUp,
|
||||||
import { fetchUserPosts, fetchUserProfile, publishPost, vote, fetchComments } from '../utils/nostr';
|
ArrowDown,
|
||||||
import { formatDistanceToNow, format } from 'date-fns';
|
MessageSquare,
|
||||||
import { pl } from 'date-fns/locale';
|
Send,
|
||||||
|
Reply,
|
||||||
|
User,
|
||||||
|
Calendar,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useStore } from "../store/useStore";
|
||||||
|
import {
|
||||||
|
fetchUserPosts,
|
||||||
|
fetchUserProfile,
|
||||||
|
publishPost,
|
||||||
|
vote,
|
||||||
|
fetchComments,
|
||||||
|
} from "../utils/nostr";
|
||||||
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
|
import { pl } from "date-fns/locale";
|
||||||
|
|
||||||
function Profile() {
|
function Profile() {
|
||||||
const { pubkey } = useParams();
|
const { pubkey } = useParams();
|
||||||
|
@ -17,7 +31,7 @@ function Profile() {
|
||||||
const [newComments, setNewComments] = useState({});
|
const [newComments, setNewComments] = useState({});
|
||||||
const [publishingComments, setPublishingComments] = useState({});
|
const [publishingComments, setPublishingComments] = useState({});
|
||||||
const { publicKey, privateKey } = useStore();
|
const { publicKey, privateKey } = useStore();
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState("all");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProfileData();
|
loadProfileData();
|
||||||
|
@ -28,12 +42,12 @@ function Profile() {
|
||||||
try {
|
try {
|
||||||
const [userProfile, userPosts] = await Promise.all([
|
const [userProfile, userPosts] = await Promise.all([
|
||||||
fetchUserProfile(pubkey),
|
fetchUserProfile(pubkey),
|
||||||
fetchUserPosts(pubkey)
|
fetchUserPosts(pubkey),
|
||||||
]);
|
]);
|
||||||
setProfile(userProfile);
|
setProfile(userProfile);
|
||||||
setPosts(userPosts.sort((a, b) => b.createdAt - a.createdAt));
|
setPosts(userPosts.sort((a, b) => b.createdAt - a.createdAt));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading profile data:', error);
|
console.error("Error loading profile data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -42,14 +56,14 @@ function Profile() {
|
||||||
async function handleVote(postId, postAuthor, isUpvote) {
|
async function handleVote(postId, postAuthor, isUpvote) {
|
||||||
if (!privateKey || votingStates[postId]) return;
|
if (!privateKey || votingStates[postId]) return;
|
||||||
|
|
||||||
setVotingStates(prev => ({ ...prev, [postId]: true }));
|
setVotingStates((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await vote(postId, postAuthor, isUpvote, privateKey);
|
await vote(postId, postAuthor, isUpvote, privateKey);
|
||||||
await loadProfileData();
|
await loadProfileData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error voting:', error);
|
console.error("Error voting:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setVotingStates(prev => ({ ...prev, [postId]: false }));
|
setVotingStates((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,13 +71,13 @@ function Profile() {
|
||||||
if (!expandedComments[postId]) {
|
if (!expandedComments[postId]) {
|
||||||
try {
|
try {
|
||||||
const fetchedComments = await fetchComments(postId);
|
const fetchedComments = await fetchComments(postId);
|
||||||
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
|
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
|
||||||
setExpandedComments(prev => ({ ...prev, [postId]: true }));
|
setExpandedComments((prev) => ({ ...prev, [postId]: true }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching comments:', error);
|
console.error("Error fetching comments:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setExpandedComments(prev => ({ ...prev, [postId]: false }));
|
setExpandedComments((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,28 +85,28 @@ function Profile() {
|
||||||
if (!newComments[postId]?.trim() || !privateKey) return;
|
if (!newComments[postId]?.trim() || !privateKey) return;
|
||||||
|
|
||||||
if (newComments[postId].length > 280) {
|
if (newComments[postId].length > 280) {
|
||||||
alert('Komentarz nie może być dłuższy niż 280 znaków!');
|
alert("Komentarz nie może być dłuższy niż 280 znaków!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPublishingComments(prev => ({ ...prev, [postId]: true }));
|
setPublishingComments((prev) => ({ ...prev, [postId]: true }));
|
||||||
try {
|
try {
|
||||||
await publishPost(newComments[postId], privateKey, postId);
|
await publishPost(newComments[postId], privateKey, postId);
|
||||||
setNewComments(prev => ({ ...prev, [postId]: '' }));
|
setNewComments((prev) => ({ ...prev, [postId]: "" }));
|
||||||
const fetchedComments = await fetchComments(postId);
|
const fetchedComments = await fetchComments(postId);
|
||||||
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
|
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
|
||||||
await loadProfileData();
|
await loadProfileData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing comment:', error);
|
console.error("Error publishing comment:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPublishingComments(prev => ({ ...prev, [postId]: false }));
|
setPublishingComments((prev) => ({ ...prev, [postId]: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredPosts = posts.filter(post => {
|
const filteredPosts = posts.filter((post) => {
|
||||||
if (activeTab === 'all') return true;
|
if (activeTab === "all") return true;
|
||||||
if (activeTab === 'mikroblog') return post.tags.includes('mikroblog');
|
if (activeTab === "mikroblog") return post.tags.includes("mikroblog");
|
||||||
if (activeTab === 'main') return !post.tags.includes('mikroblog');
|
if (activeTab === "main") return !post.tags.includes("mikroblog");
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,7 +120,7 @@ function Profile() {
|
||||||
<div className="profile__header">
|
<div className="profile__header">
|
||||||
<div className="profile__avatar">
|
<div className="profile__avatar">
|
||||||
{profile?.picture ? (
|
{profile?.picture ? (
|
||||||
<img src={profile.picture} alt={profile.name || 'Avatar'} />
|
<img src={profile.picture} alt={profile.name || "Avatar"} />
|
||||||
) : (
|
) : (
|
||||||
<User size={64} />
|
<User size={64} />
|
||||||
)}
|
)}
|
||||||
|
@ -117,7 +131,10 @@ function Profile() {
|
||||||
<div className="profile__meta">
|
<div className="profile__meta">
|
||||||
<span>
|
<span>
|
||||||
<Calendar size={16} />
|
<Calendar size={16} />
|
||||||
Dołączył(a): {format(profile?.created_at || Date.now(), 'MMMM yyyy', { locale: pl })}
|
Dołączył(a):{" "}
|
||||||
|
{format(profile?.created_at || Date.now(), "MMMM yyyy", {
|
||||||
|
locale: pl,
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,20 +144,23 @@ function Profile() {
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<button
|
<button
|
||||||
className={`tab ${activeTab === 'all' ? 'tab--active' : ''}`}
|
type="button"
|
||||||
onClick={() => setActiveTab('all')}
|
className={`tab ${activeTab === "all" ? "tab--active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("all")}
|
||||||
>
|
>
|
||||||
Wszystkie wpisy
|
Wszystkie wpisy
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab ${activeTab === 'mikroblog' ? 'tab--active' : ''}`}
|
type="button"
|
||||||
onClick={() => setActiveTab('mikroblog')}
|
className={`tab ${activeTab === "mikroblog" ? "tab--active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("mikroblog")}
|
||||||
>
|
>
|
||||||
Mikroblog
|
Mikroblog
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab ${activeTab === 'main' ? 'tab--active' : ''}`}
|
type="button"
|
||||||
onClick={() => setActiveTab('main')}
|
className={`tab ${activeTab === "main" ? "tab--active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("main")}
|
||||||
>
|
>
|
||||||
Główna
|
Główna
|
||||||
</button>
|
</button>
|
||||||
|
@ -153,7 +173,8 @@ function Profile() {
|
||||||
<div key={post.id} className="post">
|
<div key={post.id} className="post">
|
||||||
<div className="post__votes">
|
<div className="post__votes">
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[post.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
|
className={`button ${!publicKey || votingStates[post.id] ? "button--disabled" : ""}`}
|
||||||
onClick={() => handleVote(post.id, post.author, true)}
|
onClick={() => handleVote(post.id, post.author, true)}
|
||||||
disabled={!publicKey || votingStates[post.id]}
|
disabled={!publicKey || votingStates[post.id]}
|
||||||
>
|
>
|
||||||
|
@ -161,7 +182,8 @@ function Profile() {
|
||||||
</button>
|
</button>
|
||||||
<span>{post.votes.up - post.votes.down}</span>
|
<span>{post.votes.up - post.votes.down}</span>
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[post.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
|
className={`button ${!publicKey || votingStates[post.id] ? "button--disabled" : ""}`}
|
||||||
onClick={() => handleVote(post.id, post.author, false)}
|
onClick={() => handleVote(post.id, post.author, false)}
|
||||||
disabled={!publicKey || votingStates[post.id]}
|
disabled={!publicKey || votingStates[post.id]}
|
||||||
>
|
>
|
||||||
|
@ -174,11 +196,12 @@ function Profile() {
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNow(post.createdAt * 1000, {
|
{formatDistanceToNow(post.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleExpandComments(post.id)}
|
onClick={() => handleExpandComments(post.id)}
|
||||||
className="button button--link"
|
className="button button--link"
|
||||||
>
|
>
|
||||||
|
@ -193,45 +216,59 @@ function Profile() {
|
||||||
<div className="comments__form">
|
<div className="comments__form">
|
||||||
<div className="post-input-wrapper">
|
<div className="post-input-wrapper">
|
||||||
<textarea
|
<textarea
|
||||||
value={newComments[post.id] || ''}
|
value={newComments[post.id] || ""}
|
||||||
onChange={(e) => setNewComments(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setNewComments((prev) => ({
|
||||||
[post.id]: e.target.value
|
...prev,
|
||||||
}))}
|
[post.id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
placeholder="Napisz komentarz... (max 280 znaków)"
|
placeholder="Napisz komentarz... (max 280 znaków)"
|
||||||
className="post-input"
|
className="post-input"
|
||||||
rows="2"
|
rows="2"
|
||||||
maxLength={280}
|
maxLength={280}
|
||||||
/>
|
/>
|
||||||
<span className="character-count">
|
<span className="character-count">
|
||||||
{(newComments[post.id] || '').length}/280
|
{(newComments[post.id] || "").length}/280
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handlePublishComment(post.id)}
|
onClick={() => handlePublishComment(post.id)}
|
||||||
disabled={publishingComments[post.id] || !newComments[post.id]?.trim()}
|
disabled={
|
||||||
|
publishingComments[post.id] ||
|
||||||
|
!newComments[post.id]?.trim()
|
||||||
|
}
|
||||||
className="button"
|
className="button"
|
||||||
>
|
>
|
||||||
<Reply size={16} />
|
<Reply size={16} />
|
||||||
{publishingComments[post.id] ? 'Wysyłanie...' : 'Odpowiedz'}
|
{publishingComments[post.id]
|
||||||
|
? "Wysyłanie..."
|
||||||
|
: "Odpowiedz"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{comments[post.id]?.map(comment => (
|
{comments[post.id]?.map((comment) => (
|
||||||
<div key={comment.id} className="comment">
|
<div key={comment.id} className="comment">
|
||||||
<div className="comment__votes">
|
<div className="comment__votes">
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
onClick={() => handleVote(comment.id, comment.author, true)}
|
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVote(comment.id, comment.author, true)
|
||||||
|
}
|
||||||
disabled={!publicKey || votingStates[comment.id]}
|
disabled={!publicKey || votingStates[comment.id]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={16} />
|
<ArrowUp size={16} />
|
||||||
</button>
|
</button>
|
||||||
<span>{comment.votes.up - comment.votes.down}</span>
|
<span>{comment.votes.up - comment.votes.down}</span>
|
||||||
<button
|
<button
|
||||||
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
|
type="button"
|
||||||
onClick={() => handleVote(comment.id, comment.author, false)}
|
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVote(comment.id, comment.author, false)
|
||||||
|
}
|
||||||
disabled={!publicKey || votingStates[comment.id]}
|
disabled={!publicKey || votingStates[comment.id]}
|
||||||
>
|
>
|
||||||
<ArrowDown size={16} />
|
<ArrowDown size={16} />
|
||||||
|
@ -247,7 +284,7 @@ function Profile() {
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNow(comment.createdAt * 1000, {
|
{formatDistanceToNow(comment.createdAt * 1000, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: pl
|
locale: pl,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -265,4 +302,4 @@ function Profile() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { create } from 'zustand';
|
import { create } from "zustand";
|
||||||
|
|
||||||
export const useStore = create((set) => ({
|
export const useStore = create((set) => ({
|
||||||
publicKey: null,
|
publicKey: null,
|
||||||
|
@ -7,4 +7,4 @@ export const useStore = create((set) => ({
|
||||||
setKeys: (publicKey, privateKey) => set({ publicKey, privateKey }),
|
setKeys: (publicKey, privateKey) => set({ publicKey, privateKey }),
|
||||||
setProfile: (profile) => set({ profile }),
|
setProfile: (profile) => set({ profile }),
|
||||||
logout: () => set({ publicKey: null, privateKey: null, profile: null }),
|
logout: () => set({ publicKey: null, privateKey: null, profile: null }),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -35,8 +35,8 @@ body {
|
||||||
background-color: $bg-color;
|
background-color: $bg-color;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
|
@ -539,7 +539,7 @@ body {
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: "";
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
border: 2px solid $border-color;
|
border: 2px solid $border-color;
|
||||||
|
@ -558,4 +558,4 @@ body {
|
||||||
// Text utilities
|
// Text utilities
|
||||||
.text-light {
|
.text-light {
|
||||||
color: $text-light;
|
color: $text-light;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ export async function publishPost(
|
||||||
event.tags.push(["e", replyTo]);
|
event.tags.push(["e", replyTo]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// event.id = getEventHash(event);
|
|
||||||
// event.sig = await signEvent(event, privateKey);
|
|
||||||
const signedEvent = finalizeEvent(event, privateKey);
|
const signedEvent = finalizeEvent(event, privateKey);
|
||||||
|
|
||||||
const pubs = pool.publish(RELAYS, signedEvent);
|
const pubs = pool.publish(RELAYS, signedEvent);
|
||||||
|
@ -61,8 +59,6 @@ export async function vote(postId, postAuthor, isUpvote, privateKey) {
|
||||||
pubkey: "",
|
pubkey: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// event.id = getEventHash(event);
|
|
||||||
// event.sig = await signEvent(event, privateKey);
|
|
||||||
const signedEvent = finalizeEvent(event, privateKey);
|
const signedEvent = finalizeEvent(event, privateKey);
|
||||||
|
|
||||||
const pubs = pool.publish(RELAYS, signedEvent);
|
const pubs = pool.publish(RELAYS, signedEvent);
|
||||||
|
@ -360,8 +356,6 @@ export async function updateUserRole(userId, role) {
|
||||||
pubkey: "",
|
pubkey: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// event.id = getEventHash(event);
|
|
||||||
// event.sig = await signEvent(event, privateKey);
|
|
||||||
const signedEvent = finalizeEvent(event, privateKey);
|
const signedEvent = finalizeEvent(event, privateKey);
|
||||||
|
|
||||||
const pubs = pool.publish(RELAYS, signedEvent);
|
const pubs = pool.publish(RELAYS, signedEvent);
|
||||||
|
@ -387,8 +381,6 @@ export async function banUser(userId, isBanned) {
|
||||||
pubkey: "",
|
pubkey: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// event.id = getEventHash(event);
|
|
||||||
// event.sig = await signEvent(event, privateKey);
|
|
||||||
const signedEvent = finalizeEvent(event, privateKey);
|
const signedEvent = finalizeEvent(event, privateKey);
|
||||||
|
|
||||||
const pubs = pool.publish(RELAYS, signedEvent);
|
const pubs = pool.publish(RELAYS, signedEvent);
|
||||||
|
@ -414,8 +406,6 @@ export async function removePost(postId) {
|
||||||
pubkey: "",
|
pubkey: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// event.id = getEventHash(event);
|
|
||||||
// event.sig = await signEvent(event, privateKey);
|
|
||||||
const signedEvent = finalizeEvent(event, privateKey);
|
const signedEvent = finalizeEvent(event, privateKey);
|
||||||
|
|
||||||
const pubs = pool.publish(RELAYS, signedEvent);
|
const pubs = pool.publish(RELAYS, signedEvent);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react';
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue