This commit is contained in:
Sebastian Korotkiewicz 2025-04-10 12:23:10 +02:00
parent 06162e4f66
commit 3d3ee3a087
13 changed files with 332 additions and 238 deletions

View file

@ -1,13 +1,13 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Home from './pages/Home';
import Login from './pages/Login';
import Mikroblog from './pages/Mikroblog';
import Profile from './pages/Profile';
import AdminPanel from './pages/AdminPanel';
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Mikroblog from "./pages/Mikroblog";
import Profile from "./pages/Profile";
import AdminPanel from "./pages/AdminPanel";
// import Settings from './pages/Settings';
import './styles/main.scss';
import "./styles/main.scss";
function App() {
return (

View file

@ -1,7 +1,14 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { LogIn, LogOut, Home, MessageSquare, Settings, Shield } from 'lucide-react';
import { useStore } from '../store/useStore';
import React from "react";
import { Link } from "react-router-dom";
import {
LogIn,
LogOut,
Home,
MessageSquare,
Settings,
Shield,
} from "lucide-react";
import { useStore } from "../store/useStore";
function Header() {
const { publicKey, profile, logout } = useStore();
@ -24,7 +31,7 @@ function Header() {
<div className="header__nav">
{publicKey ? (
<>
{(profile?.role === 'admin' || profile?.role === 'moderator') && (
{(profile?.role === "admin" || profile?.role === "moderator") && (
<Link to="/admin">
<Shield size={16} />
Panel
@ -33,7 +40,7 @@ function Header() {
<Link to="/settings">
<Settings size={20} />
</Link>
<button onClick={logout} className="button">
<button type="button" onClick={logout} className="button">
<LogOut size={20} />
</button>
</>

View file

@ -1,9 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById('root')).render(
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
</StrictMode>,
);

View file

@ -1,23 +1,40 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Shield, User, MessageSquare, 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';
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Shield,
User,
MessageSquare,
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() {
const navigate = useNavigate();
const { publicKey, profile } = useStore();
const [activeTab, setActiveTab] = useState('users');
const [activeTab, setActiveTab] = useState("users");
const [users, setUsers] = useState([]);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [processingAction, setProcessingAction] = useState({});
useEffect(() => {
if (!publicKey || !profile || (profile.role !== 'admin' && profile.role !== 'moderator')) {
navigate('/');
if (
!publicKey ||
!profile ||
(profile.role !== "admin" && profile.role !== "moderator")
) {
navigate("/");
return;
}
@ -29,12 +46,12 @@ function AdminPanel() {
try {
const [fetchedUsers, fetchedPosts] = await Promise.all([
fetchAllUsers(),
fetchAllPosts()
fetchAllPosts(),
]);
setUsers(fetchedUsers);
setPosts(fetchedPosts);
} catch (error) {
console.error('Error loading admin data:', error);
console.error("Error loading admin data:", error);
} finally {
setLoading(false);
}
@ -43,42 +60,42 @@ function AdminPanel() {
async function handleUpdateRole(userId, newRole) {
if (processingAction[userId]) return;
setProcessingAction(prev => ({ ...prev, [userId]: true }));
setProcessingAction((prev) => ({ ...prev, [userId]: true }));
try {
await updateUserRole(userId, newRole);
await loadData();
} catch (error) {
console.error('Error updating user role:', error);
console.error("Error updating user role:", error);
} finally {
setProcessingAction(prev => ({ ...prev, [userId]: false }));
setProcessingAction((prev) => ({ ...prev, [userId]: false }));
}
}
async function handleBanUser(userId, isBanned) {
if (processingAction[userId]) return;
setProcessingAction(prev => ({ ...prev, [userId]: true }));
setProcessingAction((prev) => ({ ...prev, [userId]: true }));
try {
await banUser(userId, isBanned);
await loadData();
} catch (error) {
console.error('Error updating user ban status:', error);
console.error("Error updating user ban status:", error);
} finally {
setProcessingAction(prev => ({ ...prev, [userId]: false }));
setProcessingAction((prev) => ({ ...prev, [userId]: false }));
}
}
async function handleRemovePost(postId) {
if (processingAction[postId]) return;
setProcessingAction(prev => ({ ...prev, [postId]: true }));
setProcessingAction((prev) => ({ ...prev, [postId]: true }));
try {
await removePost(postId);
await loadData();
} catch (error) {
console.error('Error removing post:', error);
console.error("Error removing post:", error);
} finally {
setProcessingAction(prev => ({ ...prev, [postId]: false }));
setProcessingAction((prev) => ({ ...prev, [postId]: false }));
}
}
@ -92,28 +109,30 @@ function AdminPanel() {
<div className="admin-panel__header">
<h1>
<Shield size={24} />
Panel {profile?.role === 'admin' ? 'Administratora' : 'Moderatora'}
Panel {profile?.role === "admin" ? "Administratora" : "Moderatora"}
</h1>
</div>
<div className="tabs">
<button
className={`tab ${activeTab === 'users' ? 'tab--active' : ''}`}
onClick={() => setActiveTab('users')}
type="button"
className={`tab ${activeTab === "users" ? "tab--active" : ""}`}
onClick={() => setActiveTab("users")}
>
<User size={16} />
Użytkownicy
</button>
<button
className={`tab ${activeTab === 'posts' ? 'tab--active' : ''}`}
onClick={() => setActiveTab('posts')}
type="button"
className={`tab ${activeTab === "posts" ? "tab--active" : ""}`}
onClick={() => setActiveTab("posts")}
>
<MessageSquare size={16} />
Wpisy
</button>
</div>
{activeTab === 'users' ? (
{activeTab === "users" ? (
<div className="admin-panel__users">
<table className="table">
<thead>
@ -125,8 +144,11 @@ function AdminPanel() {
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id} className={user.banned ? 'table__row--banned' : ''}>
{users.map((user) => (
<tr
key={user.id}
className={user.banned ? "table__row--banned" : ""}
>
<td>
<div className="table__user">
{user.picture ? (
@ -140,7 +162,7 @@ function AdminPanel() {
</div>
</div>
</td>
<td>{user.role || 'user'}</td>
<td>{user.role || "user"}</td>
<td>
{user.banned ? (
<span className="badge badge--danger">
@ -156,10 +178,12 @@ function AdminPanel() {
</td>
<td>
<div className="table__actions">
{profile?.role === 'admin' && (
{profile?.role === "admin" && (
<select
value={user.role || 'user'}
onChange={(e) => handleUpdateRole(user.id, e.target.value)}
value={user.role || "user"}
onChange={(e) =>
handleUpdateRole(user.id, e.target.value)
}
disabled={processingAction[user.id]}
>
<option value="user">Użytkownik</option>
@ -168,7 +192,8 @@ function AdminPanel() {
</select>
)}
<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)}
disabled={processingAction[user.id]}
>
@ -203,17 +228,22 @@ function AdminPanel() {
</tr>
</thead>
<tbody>
{posts.map(post => (
{posts.map((post) => (
<tr key={post.id}>
<td>
<div className="table__user">
{post.author.picture ? (
<img src={post.author.picture} alt={post.author.name} />
<img
src={post.author.picture}
alt={post.author.name}
/>
) : (
<User size={24} />
)}
<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>
</div>
</div>
@ -222,24 +252,21 @@ function AdminPanel() {
<div className="table__content">
<p>{post.content}</p>
<div className="table__meta">
<span>
{post.votes.up - post.votes.down} punktów
</span>
<span>{post.votes.up - post.votes.down} punktów</span>
<span></span>
<span>
{post.comments} komentarzy
</span>
<span>{post.comments} komentarzy</span>
</div>
</div>
</td>
<td>
{formatDistanceToNow(post.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</td>
<td>
<button
type="button"
className="button button--danger"
onClick={() => handleRemovePost(post.id)}
disabled={processingAction[post.id]}

View file

@ -1,14 +1,14 @@
import React, { useState, useEffect } from 'react';
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from 'lucide-react';
import { useStore } from '../store/useStore';
import { fetchPosts, publishPost, vote, fetchComments } from '../utils/nostr';
import { formatDistanceToNow } from 'date-fns';
import { pl } from 'date-fns/locale';
import React, { useState, useEffect } from "react";
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from "lucide-react";
import { useStore } from "../store/useStore";
import { fetchPosts, publishPost, vote, fetchComments } from "../utils/nostr";
import { formatDistanceToNow } from "date-fns";
import { pl } from "date-fns/locale";
function Home() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [newPost, setNewPost] = useState('');
const [newPost, setNewPost] = useState("");
const [publishing, setPublishing] = useState(false);
const [votingStates, setVotingStates] = useState({});
const [expandedComments, setExpandedComments] = useState({});
@ -26,7 +26,7 @@ function Home() {
const fetchedPosts = await fetchPosts();
setPosts(fetchedPosts.sort((a, b) => b.createdAt - a.createdAt));
} catch (error) {
console.error('Error loading posts:', error);
console.error("Error loading posts:", error);
} finally {
setLoading(false);
}
@ -39,10 +39,10 @@ function Home() {
setPublishing(true);
try {
await publishPost(newPost, privateKey);
setNewPost('');
setNewPost("");
await loadPosts();
} catch (error) {
console.error('Error publishing post:', error);
console.error("Error publishing post:", error);
} finally {
setPublishing(false);
}
@ -51,14 +51,14 @@ function Home() {
async function handleVote(postId, postAuthor, isUpvote) {
if (!privateKey || votingStates[postId]) return;
setVotingStates(prev => ({ ...prev, [postId]: true }));
setVotingStates((prev) => ({ ...prev, [postId]: true }));
try {
await vote(postId, postAuthor, isUpvote, privateKey);
await loadPosts();
} catch (error) {
console.error('Error voting:', error);
console.error("Error voting:", error);
} finally {
setVotingStates(prev => ({ ...prev, [postId]: false }));
setVotingStates((prev) => ({ ...prev, [postId]: false }));
}
}
@ -66,30 +66,30 @@ function Home() {
if (!expandedComments[postId]) {
try {
const fetchedComments = await fetchComments(postId);
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
setExpandedComments(prev => ({ ...prev, [postId]: true }));
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
setExpandedComments((prev) => ({ ...prev, [postId]: true }));
} catch (error) {
console.error('Error fetching comments:', error);
console.error("Error fetching comments:", error);
}
} else {
setExpandedComments(prev => ({ ...prev, [postId]: false }));
setExpandedComments((prev) => ({ ...prev, [postId]: false }));
}
}
async function handlePublishComment(postId) {
if (!newComments[postId]?.trim() || !privateKey) return;
setPublishingComments(prev => ({ ...prev, [postId]: true }));
setPublishingComments((prev) => ({ ...prev, [postId]: true }));
try {
await publishPost(newComments[postId], privateKey, postId);
setNewComments(prev => ({ ...prev, [postId]: '' }));
setNewComments((prev) => ({ ...prev, [postId]: "" }));
const fetchedComments = await fetchComments(postId);
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
await loadPosts();
} catch (error) {
console.error('Error publishing comment:', error);
console.error("Error publishing comment:", error);
} finally {
setPublishingComments(prev => ({ ...prev, [postId]: false }));
setPublishingComments((prev) => ({ ...prev, [postId]: false }));
}
}
@ -115,7 +115,7 @@ function Home() {
className="button"
>
<Send size={20} />
{publishing ? 'Publikowanie...' : 'Opublikuj'}
{publishing ? "Publikowanie..." : "Opublikuj"}
</button>
</form>
</div>
@ -130,7 +130,8 @@ function Home() {
<div key={post.id} className="post">
<div className="post__votes">
<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)}
disabled={!publicKey || votingStates[post.id]}
>
@ -138,7 +139,8 @@ function Home() {
</button>
<span>{post.votes.up - post.votes.down}</span>
<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)}
disabled={!publicKey || votingStates[post.id]}
>
@ -153,11 +155,12 @@ function Home() {
<span>
{formatDistanceToNow(post.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</span>
<span></span>
<button
type="button"
onClick={() => handleExpandComments(post.id)}
className="button button--link"
>
@ -171,40 +174,54 @@ function Home() {
{publicKey && (
<div className="comments__form">
<textarea
value={newComments[post.id] || ''}
onChange={(e) => setNewComments(prev => ({
value={newComments[post.id] || ""}
onChange={(e) =>
setNewComments((prev) => ({
...prev,
[post.id]: e.target.value
}))}
[post.id]: e.target.value,
}))
}
placeholder="Napisz komentarz..."
className="post-input"
rows="2"
/>
<button
type="button"
onClick={() => handlePublishComment(post.id)}
disabled={publishingComments[post.id] || !newComments[post.id]?.trim()}
disabled={
publishingComments[post.id] ||
!newComments[post.id]?.trim()
}
className="button"
>
<Reply size={16} />
{publishingComments[post.id] ? 'Wysyłanie...' : 'Odpowiedz'}
{publishingComments[post.id]
? "Wysyłanie..."
: "Odpowiedz"}
</button>
</div>
)}
{comments[post.id]?.map(comment => (
{comments[post.id]?.map((comment) => (
<div key={comment.id} className="comment">
<div className="comment__votes">
<button
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
onClick={() => handleVote(comment.id, comment.author, true)}
type="button"
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
onClick={() =>
handleVote(comment.id, comment.author, true)
}
disabled={!publicKey || votingStates[comment.id]}
>
<ArrowUp size={16} />
</button>
<span>{comment.votes.up - comment.votes.down}</span>
<button
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
onClick={() => handleVote(comment.id, comment.author, false)}
type="button"
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
onClick={() =>
handleVote(comment.id, comment.author, false)
}
disabled={!publicKey || votingStates[comment.id]}
>
<ArrowDown size={16} />
@ -218,7 +235,7 @@ function Home() {
<span>
{formatDistanceToNow(comment.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</span>
</div>

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { useStore } from '../store/useStore';
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { generateSecretKey, getPublicKey } from "nostr-tools";
import { useStore } from "../store/useStore";
function Login() {
const navigate = useNavigate();
@ -14,9 +14,9 @@ function Login() {
const privateKey = generateSecretKey();
const publicKey = getPublicKey(privateKey);
setKeys(publicKey, privateKey);
navigate('/');
navigate("/");
} catch (error) {
console.error('Error generating keys:', error);
console.error("Error generating keys:", error);
}
setLoading(false);
};
@ -26,11 +26,12 @@ function Login() {
<h2>Zaloguj się</h2>
<div>
<button
type="button"
onClick={handleGenerateKeys}
disabled={loading}
className="button button--full"
>
{loading ? 'Generowanie...' : 'Wygeneruj nowy klucz'}
{loading ? "Generowanie..." : "Wygeneruj nowy klucz"}
</button>
<p className="text-light">
Twój klucz prywatny zostanie bezpiecznie zapisany w przeglądarce

View file

@ -1,14 +1,14 @@
import React, { useState, useEffect } from 'react';
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from 'lucide-react';
import { useStore } from '../store/useStore';
import { fetchPosts, publishPost, vote, fetchComments } from '../utils/nostr';
import { formatDistanceToNow } from 'date-fns';
import { pl } from 'date-fns/locale';
import React, { useState, useEffect } from "react";
import { ArrowUp, ArrowDown, MessageSquare, Send, Reply } from "lucide-react";
import { useStore } from "../store/useStore";
import { fetchPosts, publishPost, vote, fetchComments } from "../utils/nostr";
import { formatDistanceToNow } from "date-fns";
import { pl } from "date-fns/locale";
function Mikroblog() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [newPost, setNewPost] = useState('');
const [newPost, setNewPost] = useState("");
const [publishing, setPublishing] = useState(false);
const [votingStates, setVotingStates] = useState({});
const [expandedComments, setExpandedComments] = useState({});
@ -23,10 +23,10 @@ function Mikroblog() {
async function loadPosts() {
try {
const fetchedPosts = await fetchPosts('mikroblog');
const fetchedPosts = await fetchPosts("mikroblog");
setPosts(fetchedPosts.sort((a, b) => b.createdAt - a.createdAt));
} catch (error) {
console.error('Error loading posts:', error);
console.error("Error loading posts:", error);
} finally {
setLoading(false);
}
@ -37,17 +37,17 @@ function Mikroblog() {
if (!newPost.trim() || !privateKey) return;
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;
}
setPublishing(true);
try {
await publishPost(newPost, privateKey, null, 'mikroblog');
setNewPost('');
await publishPost(newPost, privateKey, null, "mikroblog");
setNewPost("");
await loadPosts();
} catch (error) {
console.error('Error publishing post:', error);
console.error("Error publishing post:", error);
} finally {
setPublishing(false);
}
@ -56,14 +56,14 @@ function Mikroblog() {
async function handleVote(postId, postAuthor, isUpvote) {
if (!privateKey || votingStates[postId]) return;
setVotingStates(prev => ({ ...prev, [postId]: true }));
setVotingStates((prev) => ({ ...prev, [postId]: true }));
try {
await vote(postId, postAuthor, isUpvote, privateKey);
await loadPosts();
} catch (error) {
console.error('Error voting:', error);
console.error("Error voting:", error);
} finally {
setVotingStates(prev => ({ ...prev, [postId]: false }));
setVotingStates((prev) => ({ ...prev, [postId]: false }));
}
}
@ -71,13 +71,13 @@ function Mikroblog() {
if (!expandedComments[postId]) {
try {
const fetchedComments = await fetchComments(postId);
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
setExpandedComments(prev => ({ ...prev, [postId]: true }));
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
setExpandedComments((prev) => ({ ...prev, [postId]: true }));
} catch (error) {
console.error('Error fetching comments:', error);
console.error("Error fetching comments:", error);
}
} 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].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;
}
setPublishingComments(prev => ({ ...prev, [postId]: true }));
setPublishingComments((prev) => ({ ...prev, [postId]: true }));
try {
await publishPost(newComments[postId], privateKey, postId, 'mikroblog');
setNewComments(prev => ({ ...prev, [postId]: '' }));
await publishPost(newComments[postId], privateKey, postId, "mikroblog");
setNewComments((prev) => ({ ...prev, [postId]: "" }));
const fetchedComments = await fetchComments(postId);
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
await loadPosts();
} catch (error) {
console.error('Error publishing comment:', error);
console.error("Error publishing comment:", error);
} finally {
setPublishingComments(prev => ({ ...prev, [postId]: false }));
setPublishingComments((prev) => ({ ...prev, [postId]: false }));
}
}
@ -121,9 +121,7 @@ function Mikroblog() {
rows="3"
maxLength={280}
/>
<span className="character-count">
{newPost.length}/280
</span>
<span className="character-count">{newPost.length}/280</span>
</div>
<button
type="submit"
@ -131,7 +129,7 @@ function Mikroblog() {
className="button"
>
<Send size={20} />
{publishing ? 'Publikowanie...' : 'Opublikuj'}
{publishing ? "Publikowanie..." : "Opublikuj"}
</button>
</form>
</div>
@ -146,7 +144,8 @@ function Mikroblog() {
<div key={post.id} className="post">
<div className="post__votes">
<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)}
disabled={!publicKey || votingStates[post.id]}
>
@ -154,7 +153,8 @@ function Mikroblog() {
</button>
<span>{post.votes.up - post.votes.down}</span>
<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)}
disabled={!publicKey || votingStates[post.id]}
>
@ -169,11 +169,12 @@ function Mikroblog() {
<span>
{formatDistanceToNow(post.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</span>
<span></span>
<button
type="button"
onClick={() => handleExpandComments(post.id)}
className="button button--link"
>
@ -188,45 +189,59 @@ function Mikroblog() {
<div className="comments__form">
<div className="post-input-wrapper">
<textarea
value={newComments[post.id] || ''}
onChange={(e) => setNewComments(prev => ({
value={newComments[post.id] || ""}
onChange={(e) =>
setNewComments((prev) => ({
...prev,
[post.id]: e.target.value
}))}
[post.id]: e.target.value,
}))
}
placeholder="Napisz komentarz... (max 280 znaków)"
className="post-input"
rows="2"
maxLength={280}
/>
<span className="character-count">
{(newComments[post.id] || '').length}/280
{(newComments[post.id] || "").length}/280
</span>
</div>
<button
type="button"
onClick={() => handlePublishComment(post.id)}
disabled={publishingComments[post.id] || !newComments[post.id]?.trim()}
disabled={
publishingComments[post.id] ||
!newComments[post.id]?.trim()
}
className="button"
>
<Reply size={16} />
{publishingComments[post.id] ? 'Wysyłanie...' : 'Odpowiedz'}
{publishingComments[post.id]
? "Wysyłanie..."
: "Odpowiedz"}
</button>
</div>
)}
{comments[post.id]?.map(comment => (
{comments[post.id]?.map((comment) => (
<div key={comment.id} className="comment">
<div className="comment__votes">
<button
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
onClick={() => handleVote(comment.id, comment.author, true)}
type="button"
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
onClick={() =>
handleVote(comment.id, comment.author, true)
}
disabled={!publicKey || votingStates[comment.id]}
>
<ArrowUp size={16} />
</button>
<span>{comment.votes.up - comment.votes.down}</span>
<button
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
onClick={() => handleVote(comment.id, comment.author, false)}
type="button"
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
onClick={() =>
handleVote(comment.id, comment.author, false)
}
disabled={!publicKey || votingStates[comment.id]}
>
<ArrowDown size={16} />
@ -240,7 +255,7 @@ function Mikroblog() {
<span>
{formatDistanceToNow(comment.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</span>
</div>

View file

@ -1,10 +1,24 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowUp, ArrowDown, MessageSquare, 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';
import React, { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import {
ArrowUp,
ArrowDown,
MessageSquare,
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() {
const { pubkey } = useParams();
@ -17,7 +31,7 @@ function Profile() {
const [newComments, setNewComments] = useState({});
const [publishingComments, setPublishingComments] = useState({});
const { publicKey, privateKey } = useStore();
const [activeTab, setActiveTab] = useState('all');
const [activeTab, setActiveTab] = useState("all");
useEffect(() => {
loadProfileData();
@ -28,12 +42,12 @@ function Profile() {
try {
const [userProfile, userPosts] = await Promise.all([
fetchUserProfile(pubkey),
fetchUserPosts(pubkey)
fetchUserPosts(pubkey),
]);
setProfile(userProfile);
setPosts(userPosts.sort((a, b) => b.createdAt - a.createdAt));
} catch (error) {
console.error('Error loading profile data:', error);
console.error("Error loading profile data:", error);
} finally {
setLoading(false);
}
@ -42,14 +56,14 @@ function Profile() {
async function handleVote(postId, postAuthor, isUpvote) {
if (!privateKey || votingStates[postId]) return;
setVotingStates(prev => ({ ...prev, [postId]: true }));
setVotingStates((prev) => ({ ...prev, [postId]: true }));
try {
await vote(postId, postAuthor, isUpvote, privateKey);
await loadProfileData();
} catch (error) {
console.error('Error voting:', error);
console.error("Error voting:", error);
} finally {
setVotingStates(prev => ({ ...prev, [postId]: false }));
setVotingStates((prev) => ({ ...prev, [postId]: false }));
}
}
@ -57,13 +71,13 @@ function Profile() {
if (!expandedComments[postId]) {
try {
const fetchedComments = await fetchComments(postId);
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
setExpandedComments(prev => ({ ...prev, [postId]: true }));
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
setExpandedComments((prev) => ({ ...prev, [postId]: true }));
} catch (error) {
console.error('Error fetching comments:', error);
console.error("Error fetching comments:", error);
}
} 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].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;
}
setPublishingComments(prev => ({ ...prev, [postId]: true }));
setPublishingComments((prev) => ({ ...prev, [postId]: true }));
try {
await publishPost(newComments[postId], privateKey, postId);
setNewComments(prev => ({ ...prev, [postId]: '' }));
setNewComments((prev) => ({ ...prev, [postId]: "" }));
const fetchedComments = await fetchComments(postId);
setComments(prev => ({ ...prev, [postId]: fetchedComments }));
setComments((prev) => ({ ...prev, [postId]: fetchedComments }));
await loadProfileData();
} catch (error) {
console.error('Error publishing comment:', error);
console.error("Error publishing comment:", error);
} finally {
setPublishingComments(prev => ({ ...prev, [postId]: false }));
setPublishingComments((prev) => ({ ...prev, [postId]: false }));
}
}
const filteredPosts = posts.filter(post => {
if (activeTab === 'all') return true;
if (activeTab === 'mikroblog') return post.tags.includes('mikroblog');
if (activeTab === 'main') return !post.tags.includes('mikroblog');
const filteredPosts = posts.filter((post) => {
if (activeTab === "all") return true;
if (activeTab === "mikroblog") return post.tags.includes("mikroblog");
if (activeTab === "main") return !post.tags.includes("mikroblog");
return true;
});
@ -106,7 +120,7 @@ function Profile() {
<div className="profile__header">
<div className="profile__avatar">
{profile?.picture ? (
<img src={profile.picture} alt={profile.name || 'Avatar'} />
<img src={profile.picture} alt={profile.name || "Avatar"} />
) : (
<User size={64} />
)}
@ -117,7 +131,10 @@ function Profile() {
<div className="profile__meta">
<span>
<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>
</div>
</div>
@ -127,20 +144,23 @@ function Profile() {
<div className="card">
<div className="tabs">
<button
className={`tab ${activeTab === 'all' ? 'tab--active' : ''}`}
onClick={() => setActiveTab('all')}
type="button"
className={`tab ${activeTab === "all" ? "tab--active" : ""}`}
onClick={() => setActiveTab("all")}
>
Wszystkie wpisy
</button>
<button
className={`tab ${activeTab === 'mikroblog' ? 'tab--active' : ''}`}
onClick={() => setActiveTab('mikroblog')}
type="button"
className={`tab ${activeTab === "mikroblog" ? "tab--active" : ""}`}
onClick={() => setActiveTab("mikroblog")}
>
Mikroblog
</button>
<button
className={`tab ${activeTab === 'main' ? 'tab--active' : ''}`}
onClick={() => setActiveTab('main')}
type="button"
className={`tab ${activeTab === "main" ? "tab--active" : ""}`}
onClick={() => setActiveTab("main")}
>
Główna
</button>
@ -153,7 +173,8 @@ function Profile() {
<div key={post.id} className="post">
<div className="post__votes">
<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)}
disabled={!publicKey || votingStates[post.id]}
>
@ -161,7 +182,8 @@ function Profile() {
</button>
<span>{post.votes.up - post.votes.down}</span>
<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)}
disabled={!publicKey || votingStates[post.id]}
>
@ -174,11 +196,12 @@ function Profile() {
<span>
{formatDistanceToNow(post.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</span>
<span></span>
<button
type="button"
onClick={() => handleExpandComments(post.id)}
className="button button--link"
>
@ -193,45 +216,59 @@ function Profile() {
<div className="comments__form">
<div className="post-input-wrapper">
<textarea
value={newComments[post.id] || ''}
onChange={(e) => setNewComments(prev => ({
value={newComments[post.id] || ""}
onChange={(e) =>
setNewComments((prev) => ({
...prev,
[post.id]: e.target.value
}))}
[post.id]: e.target.value,
}))
}
placeholder="Napisz komentarz... (max 280 znaków)"
className="post-input"
rows="2"
maxLength={280}
/>
<span className="character-count">
{(newComments[post.id] || '').length}/280
{(newComments[post.id] || "").length}/280
</span>
</div>
<button
type="button"
onClick={() => handlePublishComment(post.id)}
disabled={publishingComments[post.id] || !newComments[post.id]?.trim()}
disabled={
publishingComments[post.id] ||
!newComments[post.id]?.trim()
}
className="button"
>
<Reply size={16} />
{publishingComments[post.id] ? 'Wysyłanie...' : 'Odpowiedz'}
{publishingComments[post.id]
? "Wysyłanie..."
: "Odpowiedz"}
</button>
</div>
)}
{comments[post.id]?.map(comment => (
{comments[post.id]?.map((comment) => (
<div key={comment.id} className="comment">
<div className="comment__votes">
<button
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
onClick={() => handleVote(comment.id, comment.author, true)}
type="button"
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
onClick={() =>
handleVote(comment.id, comment.author, true)
}
disabled={!publicKey || votingStates[comment.id]}
>
<ArrowUp size={16} />
</button>
<span>{comment.votes.up - comment.votes.down}</span>
<button
className={`button ${!publicKey || votingStates[comment.id] ? 'button--disabled' : ''}`}
onClick={() => handleVote(comment.id, comment.author, false)}
type="button"
className={`button ${!publicKey || votingStates[comment.id] ? "button--disabled" : ""}`}
onClick={() =>
handleVote(comment.id, comment.author, false)
}
disabled={!publicKey || votingStates[comment.id]}
>
<ArrowDown size={16} />
@ -247,7 +284,7 @@ function Profile() {
<span>
{formatDistanceToNow(comment.createdAt * 1000, {
addSuffix: true,
locale: pl
locale: pl,
})}
</span>
</div>

View file

@ -1,4 +1,4 @@
import { create } from 'zustand';
import { create } from "zustand";
export const useStore = create((set) => ({
publicKey: null,

View file

@ -35,8 +35,8 @@ body {
background-color: $bg-color;
color: $text-color;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
// Layout
@ -539,7 +539,7 @@ body {
height: 16rem;
&::after {
content: '';
content: "";
width: 3rem;
height: 3rem;
border: 2px solid $border-color;

View file

@ -33,8 +33,6 @@ export async function publishPost(
event.tags.push(["e", replyTo]);
}
// event.id = getEventHash(event);
// event.sig = await signEvent(event, privateKey);
const signedEvent = finalizeEvent(event, privateKey);
const pubs = pool.publish(RELAYS, signedEvent);
@ -61,8 +59,6 @@ export async function vote(postId, postAuthor, isUpvote, privateKey) {
pubkey: "",
};
// event.id = getEventHash(event);
// event.sig = await signEvent(event, privateKey);
const signedEvent = finalizeEvent(event, privateKey);
const pubs = pool.publish(RELAYS, signedEvent);
@ -360,8 +356,6 @@ export async function updateUserRole(userId, role) {
pubkey: "",
};
// event.id = getEventHash(event);
// event.sig = await signEvent(event, privateKey);
const signedEvent = finalizeEvent(event, privateKey);
const pubs = pool.publish(RELAYS, signedEvent);
@ -387,8 +381,6 @@ export async function banUser(userId, isBanned) {
pubkey: "",
};
// event.id = getEventHash(event);
// event.sig = await signEvent(event, privateKey);
const signedEvent = finalizeEvent(event, privateKey);
const pubs = pool.publish(RELAYS, signedEvent);
@ -414,8 +406,6 @@ export async function removePost(postId) {
pubkey: "",
};
// event.id = getEventHash(event);
// event.sig = await signEvent(event, privateKey);
const signedEvent = finalizeEvent(event, privateKey);
const pubs = pool.publish(RELAYS, signedEvent);

View file

@ -1,5 +1,5 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({