refactor: move properties to dedicated folder
This commit is contained in:
@ -1,115 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useProjectStore } from "@/stores/project-store";
|
|
||||||
import { useAspectRatio } from "@/hooks/use-aspect-ratio";
|
|
||||||
import { Label } from "../ui/label";
|
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
|
||||||
import { Textarea } from "../ui/textarea";
|
|
||||||
import { MediaElement, TextElement } from "@/types/timeline";
|
|
||||||
import { useMediaStore } from "@/stores/media-store";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
SelectItem,
|
|
||||||
} from "../ui/select";
|
|
||||||
import { FONT_OPTIONS, type FontFamily } from "@/constants/font-constants";
|
|
||||||
|
|
||||||
export function PropertiesPanel() {
|
|
||||||
const { activeProject } = useProjectStore();
|
|
||||||
const { getDisplayName, canvasSize } = useAspectRatio();
|
|
||||||
const { selectedElements, tracks, updateTextElement } = useTimelineStore();
|
|
||||||
const { mediaItems } = useMediaStore();
|
|
||||||
|
|
||||||
const emptyView = (
|
|
||||||
<div className="space-y-4 p-5">
|
|
||||||
{/* Media Properties */}
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<PropertyItem label="Name:" value={activeProject?.name || ""} />
|
|
||||||
<PropertyItem label="Aspect ratio:" value={getDisplayName()} />
|
|
||||||
<PropertyItem
|
|
||||||
label="Resolution:"
|
|
||||||
value={`${canvasSize.width} × ${canvasSize.height}`}
|
|
||||||
/>
|
|
||||||
<PropertyItem label="Frame rate:" value="30.00fps" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TextProperties = (element: TextElement, trackId: string) => (
|
|
||||||
<div className="space-y-6 p-5">
|
|
||||||
<Textarea
|
|
||||||
placeholder="Name"
|
|
||||||
defaultValue={element.content}
|
|
||||||
className="min-h-[4.5rem] resize-none"
|
|
||||||
onChange={(e) =>
|
|
||||||
updateTextElement(trackId, element.id, { content: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-between gap-6">
|
|
||||||
<Label className="text-xs">Font</Label>
|
|
||||||
<Select
|
|
||||||
defaultValue={element.fontFamily}
|
|
||||||
onValueChange={(value: FontFamily) =>
|
|
||||||
updateTextElement(trackId, element.id, { fontFamily: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full text-xs">
|
|
||||||
<SelectValue placeholder="Select a font" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{FONT_OPTIONS.map((font) => (
|
|
||||||
<SelectItem key={font.value} value={font.value}>
|
|
||||||
{font.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const MediaProperties = (element: MediaElement) => {
|
|
||||||
const mediaItem = mediaItems.find((item) => item.id === element.mediaId);
|
|
||||||
|
|
||||||
if (mediaItem?.type === "audio") {
|
|
||||||
return <div className="space-y-4 p-5">Audio properties</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// video or image
|
|
||||||
return <div className="space-y-4 p-5">Video/Image properties</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ElementProperties = (
|
|
||||||
<>
|
|
||||||
{selectedElements.map(({ trackId, elementId }) => {
|
|
||||||
const track = tracks.find((t) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((e) => e.id === elementId);
|
|
||||||
|
|
||||||
if (element?.type === "text") {
|
|
||||||
return <div key={elementId}>{TextProperties(element, trackId)}</div>;
|
|
||||||
}
|
|
||||||
if (element?.type === "media") {
|
|
||||||
return <div key={elementId}>{MediaProperties(element)}</div>;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea className="h-full bg-panel rounded-sm">
|
|
||||||
{selectedElements.length > 0 ? ElementProperties : emptyView}
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PropertyItem({ label, value }: { label: string; value: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Label className="text-xs text-muted-foreground">{label}</Label>
|
|
||||||
<span className="text-xs text-right">{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,5 @@
|
|||||||
|
import { MediaElement } from "@/types/timeline";
|
||||||
|
|
||||||
|
export function AudioProperties({ element }: { element: MediaElement }) {
|
||||||
|
return <div className="space-y-4 p-5">Audio properties</div>;
|
||||||
|
}
|
77
apps/web/src/components/editor/properties-panel/index.tsx
Normal file
77
apps/web/src/components/editor/properties-panel/index.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
|
import { useAspectRatio } from "@/hooks/use-aspect-ratio";
|
||||||
|
import { Label } from "../../ui/label";
|
||||||
|
import { ScrollArea } from "../../ui/scroll-area";
|
||||||
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
|
import { useMediaStore } from "@/stores/media-store";
|
||||||
|
import { AudioProperties } from "./audio-properties";
|
||||||
|
import { MediaProperties } from "./media-properties";
|
||||||
|
import { TextProperties } from "./text-properties";
|
||||||
|
|
||||||
|
export function PropertiesPanel() {
|
||||||
|
const { activeProject } = useProjectStore();
|
||||||
|
const { getDisplayName, canvasSize } = useAspectRatio();
|
||||||
|
const { selectedElements, tracks } = useTimelineStore();
|
||||||
|
const { mediaItems } = useMediaStore();
|
||||||
|
|
||||||
|
const emptyView = (
|
||||||
|
<div className="space-y-4 p-5">
|
||||||
|
{/* Media Properties */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<PropertyItem label="Name:" value={activeProject?.name || ""} />
|
||||||
|
<PropertyItem label="Aspect ratio:" value={getDisplayName()} />
|
||||||
|
<PropertyItem
|
||||||
|
label="Resolution:"
|
||||||
|
value={`${canvasSize.width} × ${canvasSize.height}`}
|
||||||
|
/>
|
||||||
|
<PropertyItem label="Frame rate:" value="30.00fps" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea className="h-full bg-panel rounded-sm">
|
||||||
|
{selectedElements.length > 0
|
||||||
|
? selectedElements.map(({ trackId, elementId }) => {
|
||||||
|
const track = tracks.find((t) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((e) => e.id === elementId);
|
||||||
|
|
||||||
|
if (element?.type === "text") {
|
||||||
|
return (
|
||||||
|
<div key={elementId}>
|
||||||
|
<TextProperties element={element} trackId={trackId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (element?.type === "media") {
|
||||||
|
const mediaItem = mediaItems.find(
|
||||||
|
(item) => item.id === element.mediaId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mediaItem?.type === "audio") {
|
||||||
|
return <AudioProperties element={element} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={elementId}>
|
||||||
|
<MediaProperties element={element} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
: emptyView}
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PropertyItem({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label className="text-xs text-muted-foreground">{label}</Label>
|
||||||
|
<span className="text-xs text-right">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { MediaElement } from "@/types/timeline";
|
||||||
|
|
||||||
|
export function MediaProperties({ element }: { element: MediaElement }) {
|
||||||
|
return <div className="space-y-4 p-5">Media properties</div>;
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { FONT_OPTIONS, FontFamily } from "@/constants/font-constants";
|
||||||
|
import { TextElement } from "@/types/timeline";
|
||||||
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
|
|
||||||
|
export function TextProperties({
|
||||||
|
element,
|
||||||
|
trackId,
|
||||||
|
}: {
|
||||||
|
element: TextElement;
|
||||||
|
trackId: string;
|
||||||
|
}) {
|
||||||
|
const { updateTextElement } = useTimelineStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-5">
|
||||||
|
<Textarea
|
||||||
|
placeholder="Name"
|
||||||
|
defaultValue={element.content}
|
||||||
|
className="min-h-[4.5rem] resize-none"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateTextElement(trackId, element.id, { content: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<Label className="text-xs">Font</Label>
|
||||||
|
<Select
|
||||||
|
defaultValue={element.fontFamily}
|
||||||
|
onValueChange={(value: FontFamily) =>
|
||||||
|
updateTextElement(trackId, element.id, { fontFamily: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full text-xs">
|
||||||
|
<SelectValue placeholder="Select a font" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FONT_OPTIONS.map((font) => (
|
||||||
|
<SelectItem key={font.value} value={font.value}>
|
||||||
|
{font.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user