Skip to content

Commit a2765de

Browse files
committed
feat: feedback section with submit form and upvoting
1 parent 6ccb639 commit a2765de

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

frontend/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Header } from './components/Header';
33
import { Footer } from './components/Footer';
44
import { TreasuryStats } from './components/TreasuryStats';
55
import { TrustIndicators } from './components/TrustIndicators';
6+
import { FeedbackSection } from './components/FeedbackSection';
67
import { TxFeed } from './components/TxFeed';
78
import { useStats } from './hooks/useStats';
89
import { useRecentTx } from './hooks/useRecentTx';
@@ -38,6 +39,9 @@ export default function App() {
3839

3940
{/* Trust Indicators */}
4041
<TrustIndicators stats={stats} />
42+
43+
{/* Feedback */}
44+
<FeedbackSection />
4145
</main>
4246

4347
<Footer />
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useState } from 'react';
2+
import { useWallet } from '@solana/wallet-adapter-react';
3+
import { useFeedback } from '../hooks/useFeedback';
4+
5+
export function FeedbackSection() {
6+
const { feedback, loading, submit, vote } = useFeedback();
7+
const { publicKey } = useWallet();
8+
const [content, setContent] = useState('');
9+
const [submitting, setSubmitting] = useState(false);
10+
const [error, setError] = useState<string | null>(null);
11+
const [votedIds, setVotedIds] = useState<Set<string>>(new Set());
12+
13+
const walletLabel = publicKey
14+
? `${publicKey.toBase58().slice(0, 4)}...${publicKey.toBase58().slice(-4)}`
15+
: null;
16+
17+
async function handleSubmit(e: React.FormEvent) {
18+
e.preventDefault();
19+
if (content.trim().length < 10) {
20+
setError('Feedback must be at least 10 characters');
21+
return;
22+
}
23+
setSubmitting(true);
24+
setError(null);
25+
try {
26+
await submit(content.trim(), walletLabel ?? undefined);
27+
setContent('');
28+
} catch (err) {
29+
setError(err instanceof Error ? err.message : 'Failed to submit');
30+
} finally {
31+
setSubmitting(false);
32+
}
33+
}
34+
35+
async function handleVote(id: string) {
36+
if (votedIds.has(id)) return;
37+
try {
38+
await vote(id);
39+
setVotedIds((prev) => new Set(prev).add(id));
40+
} catch {
41+
// already voted or rate limited — ignore
42+
}
43+
}
44+
45+
function timeAgo(dateStr: string): string {
46+
const diff = Date.now() - new Date(dateStr + 'Z').getTime();
47+
const mins = Math.floor(diff / 60000);
48+
if (mins < 1) return 'just now';
49+
if (mins < 60) return `${mins}m ago`;
50+
const hours = Math.floor(mins / 60);
51+
if (hours < 24) return `${hours}h ago`;
52+
const days = Math.floor(hours / 24);
53+
return `${days}d ago`;
54+
}
55+
56+
return (
57+
<section>
58+
<h2 className="text-xl font-bold mb-4">Feedback</h2>
59+
60+
{/* Submit form */}
61+
<form onSubmit={handleSubmit} className="mb-6">
62+
<div className="flex gap-3">
63+
<textarea
64+
value={content}
65+
onChange={(e) => setContent(e.target.value)}
66+
placeholder="Suggest a feature or improvement..."
67+
maxLength={500}
68+
rows={2}
69+
className="flex-1 bg-input-bg border border-input-border rounded-sm px-3 py-2 text-sm text-text-primary placeholder:text-text-muted resize-none focus:outline-none focus:border-primary"
70+
/>
71+
<button
72+
type="submit"
73+
disabled={submitting || content.trim().length < 10}
74+
className="px-4 py-2 bg-primary text-white text-sm font-medium rounded-sm hover:opacity-90 disabled:opacity-40 transition-opacity self-end"
75+
>
76+
{submitting ? '...' : 'Post'}
77+
</button>
78+
</div>
79+
{walletLabel && (
80+
<p className="text-xs text-text-muted mt-1">Posting as {walletLabel}</p>
81+
)}
82+
{error && <p className="text-xs text-red-400 mt-1">{error}</p>}
83+
</form>
84+
85+
{/* Feedback list */}
86+
{loading ? (
87+
<p className="text-text-muted text-sm">Loading...</p>
88+
) : feedback.length === 0 ? (
89+
<p className="text-text-muted text-sm">No feedback yet. Be the first!</p>
90+
) : (
91+
<div className="space-y-2">
92+
{feedback.map((fb) => (
93+
<div
94+
key={fb.id}
95+
className="flex items-start gap-3 bg-card-bg border border-card-border rounded-sm p-3"
96+
>
97+
<button
98+
onClick={() => handleVote(fb.id)}
99+
disabled={votedIds.has(fb.id)}
100+
className={`flex flex-col items-center min-w-[40px] pt-0.5 transition-colors ${
101+
votedIds.has(fb.id) ? 'text-primary' : 'text-text-muted hover:text-primary'
102+
}`}
103+
>
104+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
105+
<path d="M8 4L3 10h10L8 4z" />
106+
</svg>
107+
<span className="text-xs font-medium">{fb.votes}</span>
108+
</button>
109+
<div className="flex-1 min-w-0">
110+
<p className="text-sm text-text-primary">{fb.content}</p>
111+
<p className="text-xs text-text-muted mt-1">
112+
{fb.author ?? 'Anonymous'} &middot; {timeAgo(fb.created_at)}
113+
</p>
114+
</div>
115+
</div>
116+
))}
117+
</div>
118+
)}
119+
</section>
120+
);
121+
}

frontend/src/hooks/useFeedback.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
import { api } from '../api';
3+
import type { Feedback } from '../types';
4+
5+
export function useFeedback() {
6+
const [feedback, setFeedback] = useState<Feedback[]>([]);
7+
const [loading, setLoading] = useState(true);
8+
9+
const refresh = useCallback(async () => {
10+
try {
11+
const { feedback } = await api.getFeedback();
12+
setFeedback(feedback);
13+
} catch {
14+
// silent — non-critical feature
15+
} finally {
16+
setLoading(false);
17+
}
18+
}, []);
19+
20+
useEffect(() => { refresh(); }, [refresh]);
21+
22+
const submit = async (content: string, author?: string) => {
23+
const fb = await api.postFeedback(content, author);
24+
setFeedback((prev) => [fb, ...prev].sort((a, b) => b.votes - a.votes));
25+
return fb;
26+
};
27+
28+
const vote = async (id: string) => {
29+
await api.voteFeedback(id);
30+
setFeedback((prev) =>
31+
prev.map((f) => (f.id === id ? { ...f, votes: f.votes + 1 } : f))
32+
.sort((a, b) => b.votes - a.votes)
33+
);
34+
};
35+
36+
return { feedback, loading, submit, vote, refresh };
37+
}

0 commit comments

Comments
 (0)