Files
DiscordClone/packages/shared/src/components/MobileChannelSettingsScreen.jsx
Bryan1029384756 a6af4dda00
All checks were successful
Build and Release / build-and-release (push) Successful in 15m35s
feat: Implement SearchPanel, various mobile UI screens, and foundational shared components across applications.
2026-02-23 11:27:01 -06:00

228 lines
10 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { useConvex } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
const MobileChannelSettingsScreen = ({ channel, categories, onClose, onDelete }) => {
const [visible, setVisible] = useState(false);
const [channelName, setChannelName] = useState(channel.name);
const [channelTopic, setChannelTopic] = useState(channel.topic || '');
const [selectedCategoryId, setSelectedCategoryId] = useState(channel.categoryId || null);
const [showCategoryPicker, setShowCategoryPicker] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [saving, setSaving] = useState(false);
const convex = useConvex();
useEffect(() => {
requestAnimationFrame(() => setVisible(true));
}, []);
const handleClose = () => {
setVisible(false);
setTimeout(onClose, 250);
};
const handleNameChange = (e) => {
setChannelName(e.target.value.toLowerCase().replace(/\s+/g, '-'));
};
const hasChanges =
channelName.trim() !== channel.name ||
channelTopic.trim() !== (channel.topic || '') ||
selectedCategoryId !== (channel.categoryId || null);
const handleSave = async () => {
if (!hasChanges || saving) return;
setSaving(true);
try {
const trimmedName = channelName.trim();
if (trimmedName && trimmedName !== channel.name) {
await convex.mutation(api.channels.rename, { id: channel._id, name: trimmedName });
}
const trimmedTopic = channelTopic.trim();
if (trimmedTopic !== (channel.topic || '')) {
await convex.mutation(api.channels.updateTopic, { id: channel._id, topic: trimmedTopic });
}
if (selectedCategoryId !== (channel.categoryId || null)) {
await convex.mutation(api.channels.moveChannel, {
id: channel._id,
categoryId: selectedCategoryId || undefined,
position: 0,
});
}
handleClose();
} catch (err) {
console.error('Failed to save channel settings:', err);
alert('Failed to save: ' + err.message);
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
try {
await convex.mutation(api.channels.remove, { id: channel._id });
if (onDelete) onDelete(channel._id);
handleClose();
} catch (err) {
console.error('Failed to delete channel:', err);
alert('Failed to delete: ' + err.message);
}
};
const currentCategoryName = selectedCategoryId
? (categories || []).find(c => c._id === selectedCategoryId)?.name || 'Unknown'
: 'None';
return ReactDOM.createPortal(
<div className={`mobile-create-screen${visible ? ' visible' : ''}`}>
{/* Header */}
<div className="mobile-create-header">
<button className="mobile-create-close-btn" onClick={handleClose}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</button>
<span className="mobile-create-title">Channel Settings</span>
<button
className={`mobile-create-submit-btn${!hasChanges || saving ? ' disabled' : ''}`}
onClick={handleSave}
disabled={!hasChanges || saving}
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
{/* Body */}
<div className="mobile-create-body">
{/* Channel Name */}
<div className="mobile-create-section">
<label className="mobile-create-section-label">Channel Name</label>
<div className="mobile-create-input-wrapper">
<span className="mobile-create-input-prefix">
{channel.type === 'voice' ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.92l4.28 4.68a1 1 0 0 0 .74.32H11a1 1 0 0 0 1-1z" />
</svg>
) : '#'}
</span>
<input
className="mobile-create-input"
type="text"
placeholder="channel-name"
value={channelName}
onChange={handleNameChange}
/>
</div>
</div>
{/* Channel Topic */}
<div className="mobile-create-section">
<label className="mobile-create-section-label">
Channel Topic
<span style={{ float: 'right', fontWeight: 400, textTransform: 'none' }}>
{channelTopic.length}/1024
</span>
</label>
<div className="mobile-create-input-wrapper" style={{ alignItems: 'flex-start' }}>
<textarea
className="mobile-create-input"
placeholder="Set a topic for this channel"
value={channelTopic}
onChange={(e) => {
if (e.target.value.length <= 1024) setChannelTopic(e.target.value);
}}
rows={3}
style={{
resize: 'none',
fontFamily: 'inherit',
lineHeight: '1.4',
}}
/>
</div>
</div>
{/* Category */}
<div className="mobile-create-section">
<label className="mobile-create-section-label">Category</label>
<div
className="mobile-create-input-wrapper"
style={{ cursor: 'pointer' }}
onClick={() => setShowCategoryPicker(!showCategoryPicker)}
>
<span className="mobile-create-input" style={{ cursor: 'pointer', userSelect: 'none' }}>
{currentCategoryName}
</span>
<svg
width="20" height="20" viewBox="0 0 24 24"
fill="var(--interactive-normal)"
style={{
marginRight: 8,
flexShrink: 0,
transform: showCategoryPicker ? 'rotate(180deg)' : 'none',
transition: 'transform 0.15s',
}}
>
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z" />
</svg>
</div>
{showCategoryPicker && (
<div className="mobile-channel-settings-category-list">
<div
className={`mobile-channel-settings-category-option${!selectedCategoryId ? ' selected' : ''}`}
onClick={() => { setSelectedCategoryId(null); setShowCategoryPicker(false); }}
>
None
</div>
{(categories || []).map(cat => (
<div
key={cat._id}
className={`mobile-channel-settings-category-option${selectedCategoryId === cat._id ? ' selected' : ''}`}
onClick={() => { setSelectedCategoryId(cat._id); setShowCategoryPicker(false); }}
>
{cat.name}
</div>
))}
</div>
)}
</div>
{/* Delete Channel */}
<div className="mobile-create-section" style={{ marginTop: 16 }}>
{!confirmDelete ? (
<button
className="mobile-channel-settings-delete-btn"
onClick={() => setConfirmDelete(true)}
>
Delete Channel
</button>
) : (
<div className="mobile-channel-settings-delete-confirm">
<p style={{ color: '#ed4245', fontSize: 14, margin: '0 0 12px' }}>
Are you sure you want to delete <strong>#{channel.name}</strong>? This cannot be undone.
</p>
<div style={{ display: 'flex', gap: 8 }}>
<button
className="mobile-channel-settings-cancel-btn"
onClick={() => setConfirmDelete(false)}
>
Cancel
</button>
<button
className="mobile-channel-settings-delete-btn"
onClick={handleDelete}
>
Delete
</button>
</div>
</div>
)}
</div>
</div>
</div>,
document.body
);
};
export default MobileChannelSettingsScreen;