🔄 Simplified Permission Model & UX Improvements
🎯 Cleaner Permission System: - Collections are private/invite-only by default - Public toggle only controls community visibility (not edit permissions) - Simplified to: Creator + Invited Collaborators can edit, everyone else view-only - Only creator can delete collections 📝 UI/UX Enhancements: - Changed 'Invite User' to 'Invite Collaborator' with clearer messaging - Replaced visibility dropdown with clean public/private toggle - Updated role descriptions: 'Collaborator' (editor) and 'Viewer' - Default invitation role is now 'editor' (collaborator) - Added explanatory text about collaboration purpose 🔐 Permission Logic Updates: - Public collections: visible in community but invite-only editing - Private collections: hidden from community, invite-only editing - Removed complex visibility states (invite-only/private distinction) - Updated permission middleware for simplified model 📧 Email Template Updates: - Clearer role descriptions in invitation emails - Focus on collaboration and card management permissions - Removed owner role from invitation options 🗄️ Database Schema Updates: - Updated APIs to use is_public boolean instead of visibility enum - Maintained backward compatibility with existing data - Simplified permission checking logic The system now has a much cleaner UX: collections are collaborative workspaces that can optionally be made visible to the community! 🚀
This commit is contained in:
parent
af7593d884
commit
b917972f5e
8 changed files with 88 additions and 108 deletions
|
|
@ -3,7 +3,7 @@ import { useState } from 'react';
|
||||||
export default function BulkInviteModal({ collectionId, isOpen, onClose, onSuccess }) {
|
export default function BulkInviteModal({ collectionId, isOpen, onClose, onSuccess }) {
|
||||||
const [inviteData, setInviteData] = useState({
|
const [inviteData, setInviteData] = useState({
|
||||||
emails: '',
|
emails: '',
|
||||||
role: 'viewer',
|
role: 'editor',
|
||||||
message: ''
|
message: ''
|
||||||
});
|
});
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
|
|
@ -63,7 +63,7 @@ export default function BulkInviteModal({ collectionId, isOpen, onClose, onSucce
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setInviteData({ emails: '', role: 'viewer', message: '' });
|
setInviteData({ emails: '', role: 'editor', message: '' });
|
||||||
setResults(null);
|
setResults(null);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
@ -121,9 +121,8 @@ user4@example.com"
|
||||||
color: 'var(--text-primary)'
|
color: 'var(--text-primary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="viewer">Viewer - Can view cards and collection</option>
|
<option value="editor">Collaborator - Can add/remove cards and edit collection details</option>
|
||||||
<option value="editor">Editor - Can add/remove cards and edit details</option>
|
<option value="viewer">Viewer - Can only view the collection</option>
|
||||||
<option value="owner">Owner - Full control including permissions</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default function CollaborationManager({ collectionId, isOwner }) {
|
||||||
const [showBulkInviteModal, setShowBulkInviteModal] = useState(false);
|
const [showBulkInviteModal, setShowBulkInviteModal] = useState(false);
|
||||||
const [inviteForm, setInviteForm] = useState({
|
const [inviteForm, setInviteForm] = useState({
|
||||||
email: '',
|
email: '',
|
||||||
role: 'viewer',
|
role: 'editor',
|
||||||
message: ''
|
message: ''
|
||||||
});
|
});
|
||||||
const [inviting, setInviting] = useState(false);
|
const [inviting, setInviting] = useState(false);
|
||||||
|
|
@ -48,7 +48,7 @@ export default function CollaborationManager({ collectionId, isOwner }) {
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setShowInviteModal(false);
|
setShowInviteModal(false);
|
||||||
setInviteForm({ email: '', role: 'viewer', message: '' });
|
setInviteForm({ email: '', role: 'editor', message: '' });
|
||||||
fetchPermissions(); // Refresh the list
|
fetchPermissions(); // Refresh the list
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
|
|
@ -153,15 +153,15 @@ export default function CollaborationManager({ collectionId, isOwner }) {
|
||||||
</h3>
|
</h3>
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowInviteModal(true)}
|
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"
|
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">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<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>
|
</svg>
|
||||||
<span>Invite User</span>
|
<span>Invite Collaborator</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowBulkInviteModal(true)}
|
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"
|
className="flex items-center space-x-2 px-4 py-2 rounded-lg font-medium border transition-all duration-200 hover:shadow-md"
|
||||||
|
|
@ -254,6 +254,9 @@ export default function CollaborationManager({ collectionId, isOwner }) {
|
||||||
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||||||
Invite Collaborator
|
Invite Collaborator
|
||||||
</h2>
|
</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">
|
<form onSubmit={handleInviteUser} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -289,9 +292,8 @@ export default function CollaborationManager({ collectionId, isOwner }) {
|
||||||
color: 'var(--text-primary)'
|
color: 'var(--text-primary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="viewer">Viewer - Can view cards and collection</option>
|
<option value="editor">Collaborator - Can add/remove cards and edit collection details</option>
|
||||||
<option value="editor">Editor - Can add/remove cards and edit details</option>
|
<option value="viewer">Viewer - Can only view the collection</option>
|
||||||
<option value="owner">Owner - Full control including permissions</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function PermissionIndicator({ userRole, visibility, showTooltip = true }) {
|
export default function PermissionIndicator({ userRole, isPublic, showTooltip = true }) {
|
||||||
const [showDetails, setShowDetails] = useState(false);
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
|
||||||
const getRoleInfo = (role) => {
|
const getRoleInfo = (role) => {
|
||||||
|
|
@ -36,41 +36,26 @@ export default function PermissionIndicator({ userRole, visibility, showTooltip
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVisibilityInfo = (vis) => {
|
const getVisibilityInfo = (isPublic) => {
|
||||||
switch (vis) {
|
if (isPublic) {
|
||||||
case 'public':
|
return {
|
||||||
return {
|
icon: '🌍',
|
||||||
icon: '🌍',
|
label: 'Public',
|
||||||
label: 'Public',
|
color: '#10b981',
|
||||||
color: '#10b981',
|
description: 'Visible in community, invite-only editing'
|
||||||
description: 'Anyone can view this collection'
|
};
|
||||||
};
|
} else {
|
||||||
case 'invite-only':
|
return {
|
||||||
return {
|
icon: '🔒',
|
||||||
icon: '👥',
|
label: 'Private',
|
||||||
label: 'Invite Only',
|
color: '#6b7280',
|
||||||
color: '#f59e0b',
|
description: 'Hidden from community, invite-only editing'
|
||||||
description: 'Only invited users can access'
|
};
|
||||||
};
|
|
||||||
case 'private':
|
|
||||||
return {
|
|
||||||
icon: '🔒',
|
|
||||||
label: 'Private',
|
|
||||||
color: '#6b7280',
|
|
||||||
description: 'Only you can access this collection'
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
icon: '❓',
|
|
||||||
label: 'Unknown',
|
|
||||||
color: '#6b7280',
|
|
||||||
description: 'Visibility not set'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleInfo = getRoleInfo(userRole);
|
const roleInfo = getRoleInfo(userRole);
|
||||||
const visibilityInfo = getVisibilityInfo(visibility);
|
const visibilityInfo = getVisibilityInfo(isPublic);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -114,7 +99,7 @@ export default function PermissionIndicator({ userRole, visibility, showTooltip
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Visibility Badge */}
|
{/* Visibility Badge */}
|
||||||
{visibility && (
|
{isPublic !== undefined && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className="flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium cursor-pointer"
|
className="flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium cursor-pointer"
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export async function checkCollectionPermission(collectionId, userId, requiredPe
|
||||||
SELECT
|
SELECT
|
||||||
c.id,
|
c.id,
|
||||||
c.user_id as owner_id,
|
c.user_id as owner_id,
|
||||||
c.visibility,
|
c.is_public,
|
||||||
cp.role,
|
cp.role,
|
||||||
cp.status
|
cp.status
|
||||||
FROM collections c
|
FROM collections c
|
||||||
|
|
@ -69,12 +69,9 @@ export async function checkCollectionPermission(collectionId, userId, requiredPe
|
||||||
return { hasAccess: true, role: 'owner', collection };
|
return { hasAccess: true, role: 'owner', collection };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public collections - everyone can view
|
// Public collections - everyone can view (but not edit)
|
||||||
if (collection.visibility === 'public') {
|
if (collection.is_public && requiredPermission === 'viewer') {
|
||||||
if (requiredPermission === 'viewer') {
|
return { hasAccess: true, role: 'viewer', collection };
|
||||||
return { hasAccess: true, role: 'viewer', collection };
|
|
||||||
}
|
|
||||||
// For edit/delete operations on public collections, need explicit permission
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check explicit permissions
|
// Check explicit permissions
|
||||||
|
|
@ -85,7 +82,7 @@ export async function checkCollectionPermission(collectionId, userId, requiredPe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private or invite-only collections without permission
|
// Collections without explicit permission
|
||||||
return { hasAccess: false, reason: 'Access denied' };
|
return { hasAccess: false, reason: 'Access denied' };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -17,23 +17,28 @@ export default async function handler(req, res) {
|
||||||
// Get user ID from auth (for now, hardcoded to 1)
|
// Get user ID from auth (for now, hardcoded to 1)
|
||||||
const currentUserId = 1;
|
const currentUserId = 1;
|
||||||
|
|
||||||
// Get collections based on visibility and user permissions
|
// Get collections based on ownership, collaboration, or public visibility
|
||||||
const result = await sql`
|
const result = await sql`
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
c.*,
|
c.*,
|
||||||
u.email as creator_email,
|
u.email as creator_email,
|
||||||
COUNT(cc.card_id) as card_count,
|
COUNT(cc.card_id) as card_count,
|
||||||
COALESCE(SUM(cards.market_price * cc.quantity), 0) as total_value,
|
COALESCE(SUM(cards.market_price * cc.quantity), 0) as total_value,
|
||||||
cp.role as user_role
|
cp.role as user_role,
|
||||||
|
CASE
|
||||||
|
WHEN c.user_id = ${currentUserId} THEN 'owner'
|
||||||
|
WHEN cp.role IS NOT NULL THEN cp.role
|
||||||
|
ELSE NULL
|
||||||
|
END as effective_role
|
||||||
FROM collections c
|
FROM collections c
|
||||||
LEFT JOIN users u ON c.user_id = u.id
|
LEFT JOIN users u ON c.user_id = u.id
|
||||||
LEFT JOIN collection_cards cc ON c.id = cc.collection_id
|
LEFT JOIN collection_cards cc ON c.id = cc.collection_id
|
||||||
LEFT JOIN cards ON cc.card_id = cards.id
|
LEFT JOIN cards ON cc.card_id = cards.id
|
||||||
LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${currentUserId} AND cp.status = 'active'
|
LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${currentUserId} AND cp.status = 'active'
|
||||||
WHERE
|
WHERE
|
||||||
c.visibility = 'public' OR
|
|
||||||
c.user_id = ${currentUserId} OR
|
c.user_id = ${currentUserId} OR
|
||||||
cp.id IS NOT NULL
|
cp.id IS NOT NULL OR
|
||||||
|
(c.is_public = true)
|
||||||
GROUP BY c.id, u.email, cp.role
|
GROUP BY c.id, u.email, cp.role
|
||||||
ORDER BY c.updated_at DESC
|
ORDER BY c.updated_at DESC
|
||||||
`;
|
`;
|
||||||
|
|
@ -47,10 +52,10 @@ export default async function handler(req, res) {
|
||||||
value: parseFloat(collection.total_value) || 0,
|
value: parseFloat(collection.total_value) || 0,
|
||||||
lastViewed: collection.updated_at,
|
lastViewed: collection.updated_at,
|
||||||
createdAt: collection.created_at,
|
createdAt: collection.created_at,
|
||||||
visibility: collection.visibility || 'private',
|
isPublic: collection.is_public || false,
|
||||||
tags: collection.tags ? collection.tags.split(',') : [],
|
tags: collection.tags ? collection.tags.split(',') : [],
|
||||||
creator: collection.creator_email,
|
creator: collection.creator_email,
|
||||||
userRole: collection.user_role || (collection.user_id === currentUserId ? 'owner' : null)
|
userRole: collection.effective_role
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.status(200).json(collections);
|
res.status(200).json(collections);
|
||||||
|
|
@ -61,22 +66,18 @@ export default async function handler(req, res) {
|
||||||
}
|
}
|
||||||
} else if (req.method === 'POST') {
|
} else if (req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
const { name, description, tcg = 'MTG', visibility = 'private', image = '', tags = [] } = req.body;
|
const { name, description, tcg = 'MTG', isPublic = false, image = '', tags = [] } = req.body;
|
||||||
|
|
||||||
if (!name || !description) {
|
if (!name || !description) {
|
||||||
return res.status(400).json({ error: 'Name and description are required' });
|
return res.status(400).json({ error: 'Name and description are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['private', 'invite-only', 'public'].includes(visibility)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid visibility type' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, use user_id = 1 (should be from auth token in real implementation)
|
// For now, use user_id = 1 (should be from auth token in real implementation)
|
||||||
const userId = 1;
|
const userId = 1;
|
||||||
|
|
||||||
const result = await sql`
|
const result = await sql`
|
||||||
INSERT INTO collections (name, description, tcg, visibility, image, tags, user_id)
|
INSERT INTO collections (name, description, tcg, is_public, image, tags, user_id)
|
||||||
VALUES (${name}, ${description}, ${tcg}, ${visibility}, ${image}, ${tags.join(',')}, ${userId})
|
VALUES (${name}, ${description}, ${tcg}, ${isPublic}, ${image}, ${tags.join(',')}, ${userId})
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,18 +72,14 @@ async function handler(req, res) {
|
||||||
} else if (req.method === 'PUT') {
|
} else if (req.method === 'PUT') {
|
||||||
// Update collection - requires editor permissions
|
// Update collection - requires editor permissions
|
||||||
try {
|
try {
|
||||||
const { name, description, visibility } = req.body;
|
const { name, description, isPublic } = req.body;
|
||||||
|
|
||||||
if (!['private', 'invite-only', 'public'].includes(visibility)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid visibility type' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await sql`
|
const result = await sql`
|
||||||
UPDATE collections
|
UPDATE collections
|
||||||
SET
|
SET
|
||||||
name = ${name},
|
name = ${name},
|
||||||
description = ${description},
|
description = ${description},
|
||||||
visibility = ${visibility},
|
is_public = ${isPublic},
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = ${id}
|
WHERE id = ${id}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
|
|
@ -93,7 +89,7 @@ async function handler(req, res) {
|
||||||
await logCollectionActivity(id, req.user.userId, 'collection_updated', {
|
await logCollectionActivity(id, req.user.userId, 'collection_updated', {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
visibility
|
isPublic
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json(result.rows[0]);
|
res.status(200).json(result.rows[0]);
|
||||||
|
|
|
||||||
|
|
@ -137,22 +137,17 @@ export default async function handler(req, res) {
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<div style="margin: 30px 0;">
|
<div style="margin: 30px 0;">
|
||||||
<h3 style="color: #333;">What you can do as a ${role}:</h3>
|
<h3 style="color: #333;">What you can do as a ${role === 'editor' ? 'collaborator' : role}:</h3>
|
||||||
<ul style="color: #666; line-height: 1.8;">
|
<ul style="color: #666; line-height: 1.8;">
|
||||||
${role === 'owner' ? `
|
${role === 'editor' ? `
|
||||||
<li>Full control over the collection</li>
|
<li>Add and remove cards from the collection</li>
|
||||||
<li>Add and remove cards</li>
|
<li>Edit collection details and description</li>
|
||||||
<li>Edit collection details</li>
|
<li>View and search all collection content</li>
|
||||||
<li>Manage permissions and invite others</li>
|
<li>Help build and organize the collection</li>
|
||||||
<li>Delete the collection</li>
|
|
||||||
` : role === 'editor' ? `
|
|
||||||
<li>Add and remove cards</li>
|
|
||||||
<li>Edit collection details</li>
|
|
||||||
<li>View all collection content</li>
|
|
||||||
` : `
|
` : `
|
||||||
<li>View all collection content</li>
|
<li>View all collection content</li>
|
||||||
<li>Browse and search cards</li>
|
<li>Browse and search cards</li>
|
||||||
<li>Export collection data</li>
|
<li>See collection statistics and details</li>
|
||||||
`}
|
`}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export default function Collections() {
|
||||||
const [newCollection, setNewCollection] = useState({
|
const [newCollection, setNewCollection] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
visibility: 'private',
|
isPublic: false,
|
||||||
image: '',
|
image: '',
|
||||||
tags: []
|
tags: []
|
||||||
});
|
});
|
||||||
|
|
@ -148,7 +148,7 @@ export default function Collections() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: newCollection.name,
|
name: newCollection.name,
|
||||||
description: newCollection.description,
|
description: newCollection.description,
|
||||||
visibility: newCollection.visibility,
|
isPublic: newCollection.isPublic,
|
||||||
image: newCollection.image,
|
image: newCollection.image,
|
||||||
tags: newCollection.tags
|
tags: newCollection.tags
|
||||||
})
|
})
|
||||||
|
|
@ -164,7 +164,7 @@ export default function Collections() {
|
||||||
setNewCollection({
|
setNewCollection({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
visibility: 'private',
|
isPublic: false,
|
||||||
image: '',
|
image: '',
|
||||||
tags: []
|
tags: []
|
||||||
});
|
});
|
||||||
|
|
@ -328,7 +328,7 @@ export default function Collections() {
|
||||||
</h3>
|
</h3>
|
||||||
<PermissionIndicator
|
<PermissionIndicator
|
||||||
userRole={collection.userRole}
|
userRole={collection.userRole}
|
||||||
visibility={collection.visibility}
|
isPublic={collection.isPublic}
|
||||||
showTooltip={false}
|
showTooltip={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -460,19 +460,24 @@ export default function Collections() {
|
||||||
Add a hero image for your collection
|
Add a hero image for your collection
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border-light)' }}>
|
||||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}>
|
<div>
|
||||||
Visibility
|
<label className="text-sm font-medium" style={{ color: 'var(--text-primary-light)' }}>
|
||||||
|
Show in Community
|
||||||
|
</label>
|
||||||
|
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary-light)' }}>
|
||||||
|
Make this collection discoverable by other users
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="sr-only peer"
|
||||||
|
checked={newCollection.isPublic}
|
||||||
|
onChange={(e) => setNewCollection({...newCollection, isPublic: e.target.checked})}
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||||
</label>
|
</label>
|
||||||
<select
|
|
||||||
className="input-field"
|
|
||||||
value={newCollection.visibility}
|
|
||||||
onChange={(e) => setNewCollection({...newCollection, visibility: e.target.value})}
|
|
||||||
>
|
|
||||||
<option value="private">🔒 Private - Only you can access</option>
|
|
||||||
<option value="invite-only">👥 Invite Only - Controlled collaboration</option>
|
|
||||||
<option value="public">🌍 Public - Anyone can view</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-3 mt-6">
|
<div className="flex space-x-3 mt-6">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue