🧹 Major Codebase Cleanup: Remove Legacy React Code

🗑️ Removed Unused Files (11 files):
- 6 temporary import result JSON files
- 3 placeholder pages (community.js, analytics.js, decks.js)
- 3 unused components (CollaborationManager, ActivityLog, BulkInviteModal)
- 2 TypeScript config files (tsconfig.json, next-env.d.ts)

📦 Cleaned Up Dependencies:
- Removed 5 unused TypeScript packages
- Kept resend for future invite/notification features
- Removed 8 packages total, reduced bundle size

 Benefits:
- Cleaner codebase with only active files
- Reduced build time and bundle size
- Eliminated TypeScript overhead (project uses only JS)
- Removed legacy React patterns and unused components
- Better maintainability and clarity

The codebase is now lean and focused on active features
This commit is contained in:
Randall Stillwell 2025-07-28 09:07:36 -05:00
parent 6097af75a4
commit 308d2de365
16 changed files with 2 additions and 4788 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,172 +0,0 @@
import { useState, useEffect } from 'react';
export default function ActivityLog({ collectionId }) {
const [activities, setActivities] = useState([]);
const [loading, setLoading] = useState(true);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
if (collectionId) {
fetchActivities();
}
}, [collectionId]);
const fetchActivities = async () => {
try {
const response = await fetch(`/api/collections/${collectionId}/activity`);
if (response.ok) {
const data = await response.json();
setActivities(data);
}
} catch (error) {
console.error('Error fetching activities:', error);
} finally {
setLoading(false);
}
};
const getActivityIcon = (action) => {
switch (action) {
case 'card_added': return '';
case 'card_removed': return '';
case 'card_quantity_updated': return '<27><>';
case 'collection_updated': return '✏️';
case 'collection_deleted': return '🗑️';
case 'user_invited': return '📧';
case 'invitation_accepted': return '✅';
case 'invitation_declined': return '❌';
case 'permission_updated': return '🔐';
case 'user_removed': return '👋';
default: return '📝';
}
};
const getActivityDescription = (activity) => {
const { action, details } = activity;
switch (action) {
case 'card_added':
return `added a card (qty: ${details.quantity})`;
case 'card_removed':
return details.reason === 'quantity_zero'
? 'removed a card by setting quantity to 0'
: 'removed a card from the collection';
case 'card_quantity_updated':
return details.oldQuantity
? `updated card quantity from ${details.oldQuantity} to ${details.newQuantity}`
: `updated card quantity to ${details.newQuantity}`;
case 'collection_updated':
return 'updated collection details';
case 'collection_deleted':
return 'deleted the collection';
case 'user_invited':
return `invited ${details.email} as ${details.role}`;
case 'invitation_accepted':
return 'accepted an invitation';
case 'invitation_declined':
return 'declined an invitation';
case 'permission_updated':
return `updated user permissions to ${details.role}`;
case 'user_removed':
return 'removed a user from the collection';
default:
return action.replace(/_/g, ' ');
}
};
const formatTimeAgo = (timestamp) => {
const now = new Date();
const activityTime = new Date(timestamp);
const diffMs = now - activityTime;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return activityTime.toLocaleDateString();
};
if (loading) {
return (
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-1/4 mb-4"></div>
<div className="space-y-3">
<div className="h-8 bg-gray-300 rounded"></div>
<div className="h-8 bg-gray-300 rounded"></div>
<div className="h-8 bg-gray-300 rounded"></div>
</div>
</div>
</div>
);
}
const displayedActivities = showAll ? activities : activities.slice(0, 5);
return (
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
Recent Activity
</h3>
{activities.length > 5 && (
<button
onClick={() => setShowAll(!showAll)}
className="text-sm font-medium hover:underline"
style={{ color: 'var(--text-accent)' }}
>
{showAll ? 'Show Less' : `View All (${activities.length})`}
</button>
)}
</div>
<div className="space-y-3">
{displayedActivities.map((activity) => (
<div
key={activity.id}
className="flex items-start space-x-3 p-3 rounded-lg"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="text-lg mt-0.5">
{getActivityIcon(activity.action)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<p className="text-sm" style={{ color: 'var(--text-primary)' }}>
<span className="font-medium">{activity.user_email || 'System'}</span>
{' '}
<span>{getActivityDescription(activity)}</span>
</p>
<span className="text-xs whitespace-nowrap ml-2" style={{ color: 'var(--text-secondary)' }}>
{formatTimeAgo(activity.created_at)}
</span>
</div>
{activity.details && Object.keys(activity.details).length > 0 && (
<div className="mt-1 text-xs" style={{ color: 'var(--text-secondary)' }}>
{activity.action === 'collection_updated' && activity.details.name && (
<span>Name: "{activity.details.name}"</span>
)}
{activity.action === 'user_invited' && activity.details.message && (
<span>Message: "{activity.details.message}"</span>
)}
</div>
)}
</div>
</div>
))}
{activities.length === 0 && (
<div className="text-center py-8">
<div className="text-4xl mb-4">📝</div>
<p style={{ color: 'var(--text-secondary)' }}>
No activity yet. Start collaborating to see updates here!
</p>
</div>
)}
</div>
</div>
);
}

View file

@ -1,219 +0,0 @@
import { useState } from 'react';
export default function BulkInviteModal({ collectionId, isOpen, onClose, onSuccess }) {
const [inviteData, setInviteData] = useState({
emails: '',
role: 'editor',
message: ''
});
const [processing, setProcessing] = useState(false);
const [results, setResults] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setProcessing(true);
setResults(null);
try {
// Parse emails (split by comma, semicolon, or newline)
const emailList = inviteData.emails
.split(/[,;\n]/)
.map(email => email.trim())
.filter(email => email && email.includes('@'));
if (emailList.length === 0) {
alert('Please enter at least one valid email address');
setProcessing(false);
return;
}
// Send invitations
const invitePromises = emailList.map(email =>
fetch(`/api/collections/${collectionId}/permissions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
role: inviteData.role,
message: inviteData.message
})
}).then(async response => ({
email,
success: response.ok,
error: response.ok ? null : (await response.json()).error
}))
);
const results = await Promise.all(invitePromises);
setResults(results);
const successCount = results.filter(r => r.success).length;
if (successCount > 0 && onSuccess) {
onSuccess();
}
} catch (error) {
console.error('Bulk invite error:', error);
alert('An error occurred while sending invitations');
} finally {
setProcessing(false);
}
};
const handleClose = () => {
setInviteData({ emails: '', role: 'editor', message: '' });
setResults(null);
onClose();
};
const successCount = results ? results.filter(r => r.success).length : 0;
const failureCount = results ? results.filter(r => !r.success).length : 0;
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="p-6 rounded-2xl shadow-lg max-w-2xl w-full mx-4 max-h-90vh overflow-y-auto" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Bulk Invite Collaborators
</h2>
{!results ? (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Email Addresses
</label>
<textarea
required
value={inviteData.emails}
onChange={(e) => setInviteData({ ...inviteData, emails: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
rows="6"
placeholder="Enter email addresses separated by commas, semicolons, or new lines:
user1@example.com, user2@example.com
user3@example.com
user4@example.com"
/>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Separate multiple email addresses with commas, semicolons, or new lines
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Permission Level
</label>
<select
value={inviteData.role}
onChange={(e) => setInviteData({ ...inviteData, role: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="editor">Collaborator - Can add/remove cards and edit collection details</option>
<option value="viewer">Viewer - Can only view the collection</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Personal Message (Optional)
</label>
<textarea
value={inviteData.message}
onChange={(e) => setInviteData({ ...inviteData, message: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
rows="3"
placeholder="Add a personal message to all invitations..."
/>
</div>
<div className="flex space-x-3 mt-6">
<button
type="button"
onClick={handleClose}
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Cancel
</button>
<button
type="submit"
disabled={processing || !inviteData.emails.trim()}
className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200 disabled:opacity-50"
>
{processing ? 'Sending Invitations...' : 'Send Invitations'}
</button>
</div>
</form>
) : (
<div className="space-y-4">
<div className="text-center py-4">
<div className="text-4xl mb-4">
{failureCount === 0 ? '🎉' : '⚠️'}
</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Invitations Sent
</h3>
<p style={{ color: 'var(--text-secondary)' }}>
{successCount} successful, {failureCount} failed
</p>
</div>
<div className="max-h-60 overflow-y-auto space-y-2">
{results.map((result, index) => (
<div
key={index}
className="flex items-center justify-between p-3 rounded-lg"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="flex items-center space-x-3">
<span className="text-lg">
{result.success ? '✅' : '❌'}
</span>
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{result.email}
</span>
</div>
{result.error && (
<span className="text-sm text-red-500">
{result.error}
</span>
)}
</div>
))}
</div>
<div className="flex justify-center mt-6">
<button
onClick={handleClose}
className="px-6 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
>
Close
</button>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -1,355 +0,0 @@
import { useState, useEffect } from 'react';
import BulkInviteModal from './BulkInviteModal';
export default function CollaborationManager({ collectionId, isOwner }) {
const [permissions, setPermissions] = useState([]);
const [loading, setLoading] = useState(true);
const [showInviteModal, setShowInviteModal] = useState(false);
const [showBulkInviteModal, setShowBulkInviteModal] = useState(false);
const [inviteForm, setInviteForm] = useState({
email: '',
role: 'editor',
message: ''
});
const [inviting, setInviting] = useState(false);
useEffect(() => {
if (collectionId) {
fetchPermissions();
}
}, [collectionId]);
const fetchPermissions = async () => {
try {
const response = await fetch(`/api/collections/${collectionId}/permissions`);
if (response.ok) {
const data = await response.json();
setPermissions(data);
}
} catch (error) {
console.error('Error fetching permissions:', error);
} finally {
setLoading(false);
}
};
const handleInviteUser = async (e) => {
e.preventDefault();
setInviting(true);
try {
const response = await fetch(`/api/collections/${collectionId}/permissions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(inviteForm)
});
if (response.ok) {
setShowInviteModal(false);
setInviteForm({ email: '', role: 'editor', message: '' });
fetchPermissions(); // Refresh the list
} else {
const error = await response.json();
alert(error.error || 'Failed to send invitation');
}
} catch (error) {
alert('Network error. Please try again.');
} finally {
setInviting(false);
}
};
const handleUpdatePermission = async (userId, newRole) => {
try {
const response = await fetch(`/api/collections/${collectionId}/permissions`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId, role: newRole })
});
if (response.ok) {
fetchPermissions(); // Refresh the list
} else {
alert('Failed to update permission');
}
} catch (error) {
alert('Network error. Please try again.');
}
};
const handleRemoveUser = async (userId) => {
if (!confirm('Are you sure you want to remove this user from the collection?')) {
return;
}
try {
const response = await fetch(`/api/collections/${collectionId}/permissions`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId })
});
if (response.ok) {
fetchPermissions(); // Refresh the list
} else {
alert('Failed to remove user');
}
} catch (error) {
alert('Network error. Please try again.');
}
};
const getRoleIcon = (role) => {
switch (role) {
case 'owner': return '👑';
case 'editor': return '✏️';
case 'viewer': return '👁️';
default: return '👤';
}
};
const getStatusBadge = (status) => {
const styles = {
active: { backgroundColor: '#10b981', color: 'white' },
pending: { backgroundColor: '#f59e0b', color: 'white' },
declined: { backgroundColor: '#ef4444', color: 'white' }
};
return (
<span
className="px-2 py-1 rounded-full text-xs font-medium"
style={styles[status] || styles.active}
>
{status}
</span>
);
};
if (loading) {
return (
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-1/4 mb-4"></div>
<div className="space-y-3">
<div className="h-12 bg-gray-300 rounded"></div>
<div className="h-12 bg-gray-300 rounded"></div>
</div>
</div>
</div>
);
}
return (
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
Collaboration ({permissions.length})
</h3>
{isOwner && (
<div className="flex space-x-2">
<button
onClick={() => setShowInviteModal(true)}
className="flex items-center space-x-2 px-4 py-2 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span>Invite Collaborator</span>
</button>
<button
onClick={() => setShowBulkInviteModal(true)}
className="flex items-center space-x-2 px-4 py-2 rounded-lg font-medium border transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span>Bulk Invite</span>
</button>
</div>
)}
</div>
<div className="space-y-3">
{permissions.map((permission) => (
<div
key={permission.id}
className="flex items-center justify-between p-4 rounded-lg border"
style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border)' }}
>
<div className="flex items-center space-x-3">
<div className="text-2xl">{getRoleIcon(permission.role)}</div>
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
{permission.email}
{permission.is_pending && (
<span className="ml-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
(Pending Registration)
</span>
)}
</div>
<div className="flex items-center space-x-2">
<span className="text-sm capitalize" style={{ color: 'var(--text-secondary)' }}>
{permission.role}
</span>
{getStatusBadge(permission.status)}
</div>
</div>
</div>
{isOwner && permission.role !== 'owner' && (
<div className="flex items-center space-x-2">
<select
value={permission.role}
onChange={(e) => handleUpdatePermission(permission.user_id, e.target.value)}
className="px-3 py-1 rounded border text-sm"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="owner">Owner</option>
</select>
<button
onClick={() => handleRemoveUser(permission.user_id)}
className="p-2 rounded text-red-500 hover:bg-red-50 transition-colors"
title="Remove user"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
)}
</div>
))}
{permissions.length === 0 && (
<div className="text-center py-8">
<div className="text-4xl mb-4">👥</div>
<p style={{ color: 'var(--text-secondary)' }}>
No collaborators yet. Invite others to work on this collection together!
</p>
</div>
)}
</div>
{/* Invite Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="p-6 rounded-2xl shadow-lg max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Invite Collaborator
</h2>
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
Invite someone to help manage and add cards to this collection
</p>
<form onSubmit={handleInviteUser} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Email Address
</label>
<input
type="email"
required
value={inviteForm.email}
onChange={(e) => setInviteForm({ ...inviteForm, email: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
placeholder="Enter email address"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Permission Level
</label>
<select
value={inviteForm.role}
onChange={(e) => setInviteForm({ ...inviteForm, role: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="editor">Collaborator - Can add/remove cards and edit collection details</option>
<option value="viewer">Viewer - Can only view the collection</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Personal Message (Optional)
</label>
<textarea
value={inviteForm.message}
onChange={(e) => setInviteForm({ ...inviteForm, message: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
rows="3"
placeholder="Add a personal message to the invitation..."
/>
</div>
<div className="flex space-x-3 mt-6">
<button
type="button"
onClick={() => setShowInviteModal(false)}
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Cancel
</button>
<button
type="submit"
disabled={inviting || !inviteForm.email}
className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200 disabled:opacity-50"
>
{inviting ? 'Sending...' : 'Send Invitation'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Bulk Invite Modal */}
<BulkInviteModal
collectionId={collectionId}
isOpen={showBulkInviteModal}
onClose={() => setShowBulkInviteModal(false)}
onSuccess={() => {
fetchPermissions(); // Refresh permissions list
setShowBulkInviteModal(false);
}}
/>
</div>
);
}

View file

@ -1,34 +0,0 @@
{
"total": 0,
"successful": 3,
"failed": 0,
"sets": [
{
"code": "tfc",
"name": "The First Chapter",
"lorcastCode": "1",
"success": true,
"importedCount": 0,
"skippedCount": 216,
"total": 216
},
{
"code": "rotf",
"name": "Rise of the Floodborn",
"lorcastCode": "2",
"success": true,
"importedCount": 0,
"skippedCount": 216,
"total": 216
},
{
"code": "ink",
"name": "Into the Inklands",
"lorcastCode": "3",
"success": true,
"importedCount": 0,
"skippedCount": 226,
"total": 226
}
]
}

View file

@ -1,25 +0,0 @@
{
"total": 658,
"successful": 3,
"failed": 0,
"sets": [
{
"code": "tfc",
"name": "The First Chapter",
"success": true,
"importedCount": 216
},
{
"code": "rotf",
"name": "Rise of the Floodborn",
"success": true,
"importedCount": 216
},
{
"code": "ink",
"name": "Into the Inklands",
"success": true,
"importedCount": 226
}
]
}

View file

@ -1,34 +0,0 @@
{
"total": 0,
"successful": 3,
"failed": 0,
"sets": [
{
"code": "tfc",
"name": "The First Chapter",
"lorcastCode": "1",
"success": true,
"importedCount": 0,
"skippedCount": 216,
"total": 216
},
{
"code": "rotf",
"name": "Rise of the Floodborn",
"lorcastCode": "2",
"success": true,
"importedCount": 0,
"skippedCount": 216,
"total": 216
},
{
"code": "ink",
"name": "Into the Inklands",
"lorcastCode": "3",
"success": true,
"importedCount": 0,
"skippedCount": 226,
"total": 226
}
]
}

5
next-env.d.ts vendored
View file

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

82
package-lock.json generated
View file

@ -21,17 +21,11 @@
"resend": "^4.7.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^16.18.126",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.21",
"eslint": "^8",
"eslint-config-next": "15.4.2",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^4.9.5"
"tailwindcss": "^3.4.17"
}
},
"node_modules/@alloc/quick-lru": {
@ -997,13 +991,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -1011,24 +998,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "16.18.126",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
@ -1046,34 +1015,6 @@
"pg-types": "^2.2.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
@ -2465,13 +2406,6 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -7047,20 +6981,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View file

@ -26,16 +26,10 @@
"resend": "^4.7.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^16.18.126",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.21",
"eslint": "^8",
"eslint-config-next": "15.4.2",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^4.9.5"
"tailwindcss": "^3.4.17"
}
}

View file

@ -1,27 +0,0 @@
import Layout from '../components/Layout';
export default function Analytics() {
const user = {
email: 'me@randallstillwell.com',
role: 'user'
};
return (
<Layout user={user}>
<div className="p-6">
<div className="text-center py-12">
<div className="text-6xl mb-4">📊</div>
<h1 className="text-3xl font-bold mb-4" style={{ color: 'var(--text-primary-light)' }}>
Analytics
</h1>
<p className="text-lg mb-6" style={{ color: 'var(--text-secondary-light)' }}>
Track your collection performance and insights
</p>
<p className="text-sm" style={{ color: 'var(--text-secondary-light)' }}>
Coming soon...
</p>
</div>
</div>
</Layout>
);
}

View file

@ -1,27 +0,0 @@
import Layout from '../components/Layout';
export default function Community() {
const user = {
email: 'me@randallstillwell.com',
role: 'user'
};
return (
<Layout user={user}>
<div className="p-6">
<div className="text-center py-12">
<div className="text-6xl mb-4">👥</div>
<h1 className="text-3xl font-bold mb-4" style={{ color: 'var(--text-primary-light)' }}>
Community
</h1>
<p className="text-lg mb-6" style={{ color: 'var(--text-secondary-light)' }}>
Connect with other TCG enthusiasts
</p>
<p className="text-sm" style={{ color: 'var(--text-secondary-light)' }}>
Coming soon...
</p>
</div>
</div>
</Layout>
);
}

View file

@ -1,27 +0,0 @@
import Layout from '../components/Layout';
export default function Decks() {
const user = {
email: 'me@randallstillwell.com',
role: 'user'
};
return (
<Layout user={user}>
<div className="p-6">
<div className="text-center py-12">
<div className="text-6xl mb-4">🎴</div>
<h1 className="text-3xl font-bold mb-4" style={{ color: 'var(--text-primary-light)' }}>
Decks
</h1>
<p className="text-lg mb-6" style={{ color: 'var(--text-secondary-light)' }}>
Build and manage your card decks
</p>
<p className="text-sm" style={{ color: 'var(--text-secondary-light)' }}>
Coming soon...
</p>
</div>
</div>
</Layout>
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"es6"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"forceConsistentCasingInFileNames": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}