diff --git a/lander/app/components/FeedbackCard.tsx b/lander/app/components/FeedbackCard.tsx
deleted file mode 100644
index 36f89e828..000000000
--- a/lander/app/components/FeedbackCard.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-'use client';
-
-import type { Feedback } from '../feedback/page';
-
-interface FeedbackCardProps {
- feedback: Feedback;
- token: string | null;
- onUpdate: () => void;
- onClick: () => void;
-}
-
-export default function FeedbackCard({ feedback, token, onUpdate, onClick }: FeedbackCardProps) {
- const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
-
- const handleVote = async (voteType: 'upvote' | 'downvote') => {
- if (!token) return;
-
- try {
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/vote`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- },
- body: JSON.stringify({ voteType }),
- });
-
- const data = await response.json();
- if (data.success) {
- onUpdate();
- }
- } catch (error) {
- console.error('Failed to vote:', error);
- }
- };
-
- const getTypeLabel = (type: string) => {
- switch (type) {
- case 'feature':
- return 'FEATURE REQUEST';
- case 'bug':
- return 'BUG';
- case 'improvement':
- return 'IMPROVEMENT';
- default:
- return type.toUpperCase();
- }
- };
-
- return (
-
-
-
{feedback.title}
-
- {getTypeLabel(feedback.type)}
-
-
- {feedback.description && (
-
{feedback.description}
- )}
-
-
-
-
-
-
- {feedback.comments.length} {feedback.comments.length === 1 ? 'comment' : 'comments'}
-
-
-
- );
-}
diff --git a/lander/app/components/FeedbackDetailModal.tsx b/lander/app/components/FeedbackDetailModal.tsx
deleted file mode 100644
index be8529ac1..000000000
--- a/lander/app/components/FeedbackDetailModal.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import type { Feedback } from '../feedback/page';
-
-interface FeedbackDetailModalProps {
- feedback: Feedback;
- token: string | null;
- onClose: () => void;
- onUpdate: () => void;
- useMockData?: boolean;
- mockComments?: Record>;
- feedbacks?: Feedback[];
- setFeedbacks?: (feedbacks: Feedback[]) => void;
-}
-
-export default function FeedbackDetailModal({
- feedback,
- token,
- onClose,
- onUpdate,
- useMockData = false,
- mockComments,
- feedbacks = [],
- setFeedbacks,
-}: FeedbackDetailModalProps) {
- const [comments, setComments] = useState>([]);
- const [newComment, setNewComment] = useState('');
- const [loading, setLoading] = useState(true);
- const [currentFeedback, setCurrentFeedback] = useState(feedback);
-
- const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
-
- const loadComments = async () => {
- try {
- if (useMockData && mockComments) {
- // Mock comments
- await new Promise(resolve => setTimeout(resolve, 200));
- const feedbackComments = mockComments[feedback._id] || [];
- setComments(feedbackComments);
- } else {
- const headers: HeadersInit = {};
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
-
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}`, { headers });
- const data = await response.json();
- if (data.success) {
- setComments(data.data.comments || []);
- setCurrentFeedback(data.data);
- }
- }
- } catch (error) {
- console.error('Failed to load comments:', error);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- setCurrentFeedback(feedback);
- loadComments();
- }, [feedback._id, token]);
-
- const handleAddComment = async () => {
- if (!token || !newComment.trim()) return;
-
- try {
- if (useMockData && mockComments && setFeedbacks) {
- // Mock add comment
- await new Promise(resolve => setTimeout(resolve, 200));
- const newCommentObj = {
- _id: String(Date.now()),
- content: newComment,
- user: {
- telegramId: 123456789,
- telegramFirstName: 'John',
- telegramLastName: 'Doe',
- telegramUsername: 'johndoe',
- },
- createdAt: new Date().toISOString(),
- };
-
- if (!mockComments[feedback._id]) {
- mockComments[feedback._id] = [];
- }
- mockComments[feedback._id].push(newCommentObj);
-
- // Update feedback comments count
- const updatedFeedbacks = feedbacks.map(f => {
- if (f._id === feedback._id) {
- return { ...f, comments: [...f.comments, newCommentObj._id] };
- }
- return f;
- });
-
- // Update global mock state
- const { setMockFeedbacks } = require('../feedback/page');
- setMockFeedbacks(updatedFeedbacks);
- setFeedbacks(updatedFeedbacks);
-
- setNewComment('');
- await loadComments();
- onUpdate();
- } else {
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/comments`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- },
- body: JSON.stringify({ content: newComment }),
- });
-
- const data = await response.json();
- if (data.success) {
- setNewComment('');
- await loadComments();
- onUpdate();
- }
- }
- } catch (error) {
- console.error('Failed to add comment:', error);
- }
- };
-
- const handleVote = async (voteType: 'upvote' | 'downvote') => {
- if (!token) return;
-
- try {
- if (useMockData && setFeedbacks) {
- // Mock vote
- await new Promise(resolve => setTimeout(resolve, 200));
-
- const updatedFeedbacks = feedbacks.map(f => {
- if (f._id === feedback._id) {
- const updated = { ...f };
-
- // Toggle vote logic
- if (updated.userVote === voteType) {
- // Remove vote
- updated.userVote = null;
- if (voteType === 'upvote') {
- updated.upvotes = Math.max(0, updated.upvotes - 1);
- } else {
- updated.downvotes = Math.max(0, updated.downvotes - 1);
- }
- } else if (updated.userVote) {
- // Change vote
- const oldVote = updated.userVote;
- if (oldVote === 'upvote') {
- updated.upvotes = Math.max(0, updated.upvotes - 1);
- } else {
- updated.downvotes = Math.max(0, updated.downvotes - 1);
- }
- updated.userVote = voteType;
- if (voteType === 'upvote') {
- updated.upvotes += 1;
- } else {
- updated.downvotes += 1;
- }
- } else {
- // New vote
- updated.userVote = voteType;
- if (voteType === 'upvote') {
- updated.upvotes += 1;
- } else {
- updated.downvotes += 1;
- }
- }
-
- setCurrentFeedback(updated);
- return updated;
- }
- return f;
- });
-
- // Update global mock state
- const { setMockFeedbacks } = require('../feedback/page');
- setMockFeedbacks(updatedFeedbacks);
- setFeedbacks(updatedFeedbacks);
- onUpdate();
- } else {
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/vote`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- },
- body: JSON.stringify({ voteType }),
- });
-
- const data = await response.json();
- if (data.success) {
- setCurrentFeedback({
- ...currentFeedback,
- upvotes: data.data.upvotes,
- downvotes: data.data.downvotes,
- userVote: data.data.userVote,
- });
- onUpdate();
- }
- }
- } catch (error) {
- console.error('Failed to vote:', error);
- }
- };
-
- return (
-
-
-
-
-
-
{currentFeedback.title}
- {currentFeedback.description && (
-
{currentFeedback.description}
- )}
-
-
-
-
- {comments.length} {comments.length === 1 ? 'comment' : 'comments'}
-
-
-
-
-
-
-
-
-
-
Comments
- {loading ? (
-
Loading comments...
- ) : comments.length > 0 ? (
-
- {comments.map((comment) => (
-
-
-
- {comment.user.telegramFirstName || ''} {comment.user.telegramLastName || ''}
- {comment.user.telegramUsername && ` (@${comment.user.telegramUsername})`}
-
-
- {new Date(comment.createdAt).toLocaleDateString()}
-
-
-
{comment.content}
-
- ))}
-
- ) : (
-
No comments yet.
- )}
-
-
- {token && (
-
-
- )}
-
-
-
- );
-}
diff --git a/lander/app/components/FeedbackKanban.tsx b/lander/app/components/FeedbackKanban.tsx
deleted file mode 100644
index fcd9c2efb..000000000
--- a/lander/app/components/FeedbackKanban.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import FeedbackCard from './FeedbackCard';
-import type { Feedback } from '../feedback/page';
-
-interface FeedbackKanbanProps {
- feedbacks: Feedback[];
- token: string | null;
- onUpdate: () => void;
-}
-
-export default function FeedbackKanban({ feedbacks, token, onUpdate }: FeedbackKanbanProps) {
- const [selectedFeedback, setSelectedFeedback] = useState(null);
-
- const planned = feedbacks.filter((f) => f.status === 'planned');
- const inProgress = feedbacks.filter((f) => f.status === 'in_progress');
- const complete = feedbacks.filter((f) => f.status === 'complete');
-
- const columns = [
- {
- id: 'planned',
- title: 'Planned',
- items: planned,
- color: 'blue',
- },
- {
- id: 'in_progress',
- title: 'In Progress',
- items: inProgress,
- color: 'purple',
- },
- {
- id: 'complete',
- title: 'Complete',
- items: complete,
- color: 'green',
- },
- ];
-
- return (
- <>
-
-
-
Boards
-
-
-
-
-
-
-
-
-
-
Roadmap
-
- {columns.map((column) => (
-
-
-
- {column.items.length > 0 ? (
- column.items.map((feedback) => (
-
setSelectedFeedback(feedback)}
- />
- ))
- ) : (
-
-
💡
-
- Share your feedback and check back here for updates.
-
-
- )}
-
-
- ))}
-
-
-
- {selectedFeedback && (
- setSelectedFeedback(null)}
- onUpdate={() => {
- onUpdate();
- setSelectedFeedback(null);
- }}
- />
- )}
- >
- );
-}
-
-function FeedbackDetailModal({
- feedback,
- token,
- onClose,
- onUpdate,
-}: {
- feedback: Feedback;
- token: string | null;
- onClose: () => void;
- onUpdate: () => void;
-}) {
- const [comments, setComments] = useState>([]);
- const [newComment, setNewComment] = useState('');
- const [loading, setLoading] = useState(true);
-
- const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
-
- const loadComments = async () => {
- try {
- const headers: HeadersInit = {};
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
-
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}`, { headers });
- const data = await response.json();
- if (data.success) {
- setComments(data.data.comments || []);
- }
- } catch (error) {
- console.error('Failed to load comments:', error);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- loadComments();
- }, [feedback._id, token]);
-
- const handleAddComment = async () => {
- if (!token || !newComment.trim()) return;
-
- try {
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/comments`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- },
- body: JSON.stringify({ content: newComment }),
- });
-
- const data = await response.json();
- if (data.success) {
- setNewComment('');
- await loadComments();
- }
- } catch (error) {
- console.error('Failed to add comment:', error);
- }
- };
-
- return (
-
-
-
-
-
-
{feedback.title}
- {feedback.description && (
-
{feedback.description}
- )}
-
-
-
-
-
-
-
-
Comments
- {loading ? (
-
Loading comments...
- ) : comments.length > 0 ? (
-
- {comments.map((comment) => (
-
-
-
- {comment.user.telegramFirstName || ''} {comment.user.telegramLastName || ''}
- {comment.user.telegramUsername && ` (@${comment.user.telegramUsername})`}
-
-
- {new Date(comment.createdAt).toLocaleDateString()}
-
-
-
{comment.content}
-
- ))}
-
- ) : (
-
No comments yet.
- )}
-
-
- {token && (
-
-
- )}
-
-
-
- );
-}
diff --git a/lander/app/components/FeedbackList.tsx b/lander/app/components/FeedbackList.tsx
deleted file mode 100644
index 0e0aa0a20..000000000
--- a/lander/app/components/FeedbackList.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import type { Feedback } from '../feedback/page';
-import FeedbackDetailModal from './FeedbackDetailModal';
-
-interface FeedbackListProps {
- feedbacks: Feedback[];
- token: string | null;
- onUpdate: () => void;
- useMockData?: boolean;
- mockComments?: Record>;
- setFeedbacks?: (feedbacks: Feedback[]) => void;
-}
-
-export default function FeedbackList({ feedbacks, token, onUpdate, useMockData = false, mockComments, setFeedbacks }: FeedbackListProps) {
- const [selectedFeedback, setSelectedFeedback] = useState(null);
-
- if (feedbacks.length === 0) {
- return (
-
-
No {feedbacks.length === 0 ? 'feedback' : 'results'} found.
-
- );
- }
-
- return (
- <>
-
- {feedbacks.map((feedback) => (
- setSelectedFeedback(feedback)}
- useMockData={useMockData}
- setFeedbacks={setFeedbacks}
- allFeedbacks={feedbacks}
- />
- ))}
-
-
- {selectedFeedback && (
- setSelectedFeedback(null)}
- onUpdate={() => {
- onUpdate();
- setSelectedFeedback(null);
- }}
- useMockData={useMockData}
- mockComments={mockComments}
- feedbacks={feedbacks}
- setFeedbacks={setFeedbacks}
- />
- )}
- >
- );
-}
-
-function FeedbackListItem({
- feedback,
- token,
- onUpdate,
- onClick,
- useMockData = false,
- setFeedbacks,
- allFeedbacks,
-}: {
- feedback: Feedback;
- token: string | null;
- onUpdate: () => void;
- onClick: () => void;
- useMockData?: boolean;
- setFeedbacks?: (feedbacks: Feedback[]) => void;
- allFeedbacks: Feedback[];
-}) {
- const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
-
- const handleVote = async (e: React.MouseEvent, voteType: 'upvote' | 'downvote') => {
- e.stopPropagation();
- if (!token) return;
-
- try {
- if (useMockData && setFeedbacks) {
- // Mock vote - update local state
- await new Promise(resolve => setTimeout(resolve, 200));
-
- const updatedFeedbacks = allFeedbacks.map(f => {
- if (f._id === feedback._id) {
- const updated = { ...f };
-
- // Toggle vote logic
- if (updated.userVote === voteType) {
- // Remove vote
- updated.userVote = null;
- if (voteType === 'upvote') {
- updated.upvotes = Math.max(0, updated.upvotes - 1);
- } else {
- updated.downvotes = Math.max(0, updated.downvotes - 1);
- }
- } else if (updated.userVote) {
- // Change vote
- const oldVote = updated.userVote;
- if (oldVote === 'upvote') {
- updated.upvotes = Math.max(0, updated.upvotes - 1);
- } else {
- updated.downvotes = Math.max(0, updated.downvotes - 1);
- }
- updated.userVote = voteType;
- if (voteType === 'upvote') {
- updated.upvotes += 1;
- } else {
- updated.downvotes += 1;
- }
- } else {
- // New vote
- updated.userVote = voteType;
- if (voteType === 'upvote') {
- updated.upvotes += 1;
- } else {
- updated.downvotes += 1;
- }
- }
-
- return updated;
- }
- return f;
- });
-
- // Update global mock state
- const { setMockFeedbacks } = require('../feedback/page');
- setMockFeedbacks(updatedFeedbacks);
- setFeedbacks(updatedFeedbacks);
- } else {
- const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/vote`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- },
- body: JSON.stringify({ voteType }),
- });
-
- const data = await response.json();
- if (data.success) {
- onUpdate();
- }
- }
- } catch (error) {
- console.error('Failed to vote:', error);
- }
- };
-
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'planned':
- return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
- case 'in_progress':
- return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
- case 'complete':
- return 'bg-green-500/20 text-green-400 border-green-500/30';
- default:
- return 'bg-zinc-800 text-zinc-400 border-zinc-700';
- }
- };
-
- return (
-
-
-
-
-
{feedback.title}
-
- {feedback.status === 'in_progress' ? 'In Progress' : feedback.status.charAt(0).toUpperCase() + feedback.status.slice(1)}
-
-
- {feedback.description && (
-
{feedback.description}
- )}
-
-
-
- {feedback.comments.length} {feedback.comments.length === 1 ? 'comment' : 'comments'}
-
-
-
-
-
- );
-}