- Introduced AccessMode enum to define channel access levels: Public, PasswordProtected, AccountRequired, and OwnerOnly. - Updated Channel and ProgrammingBlock entities to include access_mode and access_password_hash fields. - Enhanced create and update channel functionality to handle access mode and password. - Implemented access checks in channel routes based on the defined access modes. - Modified frontend components to support channel creation and editing with access control options. - Added ChannelPasswordModal for handling password input when accessing restricted channels. - Updated API calls to include channel and block passwords as needed. - Created database migrations to add access_mode and access_password_hash columns to channels table.
162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import type { AccessMode } from "@/lib/types";
|
|
|
|
interface CreateChannelDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSubmit: (data: {
|
|
name: string;
|
|
timezone: string;
|
|
description: string;
|
|
access_mode?: AccessMode;
|
|
access_password?: string;
|
|
}) => void;
|
|
isPending: boolean;
|
|
error?: string | null;
|
|
}
|
|
|
|
export function CreateChannelDialog({
|
|
open,
|
|
onOpenChange,
|
|
onSubmit,
|
|
isPending,
|
|
error,
|
|
}: CreateChannelDialogProps) {
|
|
const [name, setName] = useState("");
|
|
const [timezone, setTimezone] = useState("UTC");
|
|
const [description, setDescription] = useState("");
|
|
const [accessMode, setAccessMode] = useState<AccessMode>("public");
|
|
const [accessPassword, setAccessPassword] = useState("");
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
onSubmit({
|
|
name,
|
|
timezone,
|
|
description,
|
|
access_mode: accessMode !== "public" ? accessMode : undefined,
|
|
access_password: accessMode === "password_protected" && accessPassword ? accessPassword : undefined,
|
|
});
|
|
};
|
|
|
|
const handleOpenChange = (next: boolean) => {
|
|
if (!isPending) {
|
|
onOpenChange(next);
|
|
if (!next) {
|
|
setName("");
|
|
setTimezone("UTC");
|
|
setDescription("");
|
|
setAccessMode("public");
|
|
setAccessPassword("");
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogContent className="bg-zinc-900 border-zinc-800 text-zinc-100 sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>New channel</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4 py-2">
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-zinc-400">
|
|
Name <span className="text-red-400">*</span>
|
|
</label>
|
|
<input
|
|
required
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="90s Sitcom Network"
|
|
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-zinc-400">
|
|
Timezone <span className="text-red-400">*</span>
|
|
</label>
|
|
<input
|
|
required
|
|
value={timezone}
|
|
onChange={(e) => setTimezone(e.target.value)}
|
|
placeholder="America/New_York"
|
|
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
<p className="text-[11px] text-zinc-600">
|
|
IANA timezone, e.g. America/New_York, Europe/London, UTC
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-zinc-400">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Nothing but classic sitcoms, all day"
|
|
rows={2}
|
|
className="w-full resize-none rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-zinc-400">Access</label>
|
|
<select
|
|
value={accessMode}
|
|
onChange={(e) => setAccessMode(e.target.value as AccessMode)}
|
|
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 focus:border-zinc-500 focus:outline-none"
|
|
>
|
|
<option value="public">Public</option>
|
|
<option value="password_protected">Password protected</option>
|
|
<option value="account_required">Account required</option>
|
|
<option value="owner_only">Owner only</option>
|
|
</select>
|
|
</div>
|
|
|
|
{accessMode === "password_protected" && (
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-zinc-400">Password</label>
|
|
<input
|
|
type="password"
|
|
value={accessPassword}
|
|
onChange={(e) => setAccessPassword(e.target.value)}
|
|
placeholder="Channel password"
|
|
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => handleOpenChange(false)}
|
|
disabled={isPending}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={isPending}>
|
|
{isPending ? "Creating…" : "Create channel"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|