🧹 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:
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
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
5
next-env.d.ts
vendored
|
|
@ -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
82
package-lock.json
generated
|
|
@ -21,17 +21,11 @@
|
||||||
"resend": "^4.7.0"
|
"resend": "^4.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "15.4.2",
|
"eslint-config-next": "15.4.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17"
|
||||||
"typescript": "^4.9.5"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|
@ -997,13 +991,6 @@
|
||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
|
|
@ -1011,24 +998,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.18.126",
|
"version": "16.18.126",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||||
|
|
@ -1046,34 +1015,6 @@
|
||||||
"pg-types": "^2.2.0"
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.38.0",
|
"version": "8.38.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||||
|
|
@ -2465,13 +2406,6 @@
|
||||||
"node": ">=4"
|
"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": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
|
|
@ -7047,20 +6981,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,10 @@
|
||||||
"resend": "^4.7.0"
|
"resend": "^4.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "15.4.2",
|
"eslint-config-next": "15.4.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17"
|
||||||
"typescript": "^4.9.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue