Skip to content

Commit

Permalink
feat: new episode form page tweaking + timestamp input
Browse files Browse the repository at this point in the history
  • Loading branch information
yjose committed Nov 14, 2024
1 parent 8ec0da8 commit 358277e
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 38 deletions.
11 changes: 5 additions & 6 deletions src/components/create-episode/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,13 @@ function NewEpisodeForm({
trigger,
setError,
clearErrors,
setValue,
} = useForm<FormValues>({
resolver: zodResolver(episodeSchemaForm),
defaultValues,
mode: "onChange",
});

const onSubmit = (data: FormValues) => {
console.log(data);
};

return (
<div className="grid grid-cols-2 gap-16">
<div className="space-y-2">
Expand All @@ -154,6 +151,8 @@ function NewEpisodeForm({
setError={setError}
watch={watch}
clearErrors={clearErrors}
setValue={setValue}
totalDuration={getValues("duration")}
/>

<Links
Expand All @@ -164,10 +163,9 @@ function NewEpisodeForm({
trigger={trigger}
/>
<GenerateEpisodeButton handleSubmit={handleSubmit} />
<button onClick={handleSubmit(onSubmit)}>Submit</button>
</div>
<div className="space-y-2">
<div className="aspect-h-9 aspect-w-16 w-full">
<div className="aspect-video w-full">
{defaultValues.youtube && (
<iframe
src={`https://www.youtube.com/embed/${getYoutubeId(
Expand All @@ -176,6 +174,7 @@ function NewEpisodeForm({
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
className="aspect-video w-full"
allowFullScreen
></iframe>
)}
Expand Down
77 changes: 61 additions & 16 deletions src/components/create-episode/generate-episode-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,73 @@ export const GenerateEpisodeButton = ({

<dialog
ref={dialogRef}
className="fixed left-1/2 top-1/2 w-full max-w-5xl -translate-x-1/2 -translate-y-1/2 rounded-lg p-4 backdrop:bg-gray-500/50"
className="fixed left-1/2 top-1/2 w-full max-w-5xl -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-xl backdrop:bg-gray-500/50"
>
<div className="flex flex-col gap-4">
<div className="pose max-h-[80vh] overflow-scroll bg-slate-200 p-4">
<code className="whitespace-pre-wrap break-words">{markdown}</code>
</div>
<div className="flex gap-2 self-end">
<button
onClick={() => {
navigator.clipboard.writeText(markdown);
}}
className="rounded-md bg-gray-500 px-4 py-2 text-white hover:bg-gray-600"
>
Copy to Clipboard
</button>
<div className="relative flex flex-col gap-4">
<div className="absolute right-0 top-0 flex gap-2">
<button
onClick={() => dialogRef.current?.close()}
className="rounded-md bg-gray-500 px-4 py-2 text-white hover:bg-gray-600"
className="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100"
title="Close"
>
Close
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>

<h2 className="text-xl font-semibold text-gray-800">
Generated Episode Markdown
</h2>
<h2 className="max-w-lg text-sm text-gray-600">
Copy the episode content and create a pull request in the repository
to add this episode. The file should be placed in
episodes/episode-XXXX.md
</h2>

<div className="relative mt-4 max-h-[70vh] overflow-y-auto rounded-lg border border-gray-200">
<div className="absolute right-0 top-0 flex gap-2">
<button
onClick={() => {
navigator.clipboard.writeText(markdown);
}}
className="flex items-center gap-2 rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100"
title="Copy to clipboard"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span>Copy</span>
</button>
</div>
<pre className="bg-gray-50 p-4">
<code className="block whitespace-pre-wrap break-words font-mono text-sm text-gray-800">
{markdown}
</code>
</pre>
</div>
</div>
</dialog>
</>
Expand Down
8 changes: 5 additions & 3 deletions src/components/create-episode/generate-episode-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { episodeSchemaForm } from "./schema";

type FormValues = z.infer<typeof episodeSchemaForm>;

const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);

export function generateEpisodeMarkdown(episode: FormValues): string {
const formatDate = (date: Date) => date.toISOString().split("T")[0];

Expand All @@ -17,23 +19,23 @@ published: ${episode.published}
featured: ${episode.featured}
---
${episode.description}
${capitalize(episode.description)}
## Guests
${episode.guests.map(guest => `- [${guest.title}](${guest.url})`).join("\n")}
## Notes
${episode.notes.map(note => `${note.timestamp} - ${note.content}`).join("\n\n")}
${episode.notes.map(note => `${note.timestamp} - ${capitalize(note.content)}`).join("\n\n")}
## Links
${episode.links.map(link => `- [${link.title}](${link.url})`).join("\n")}
## Prepared and Presented by
${episode.hosts.map(host => `- [${host.title}](${host.url})`).join("\n")}
${episode.hosts.map(host => `- [${capitalize(host.title)}](${host.url})`).join("\n")}
`;

return frontmatter;
Expand Down
35 changes: 27 additions & 8 deletions src/components/create-episode/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
type UseFormClearErrors,
type UseFormRegister,
type UseFormSetError,
type UseFormSetValue,
type UseFormWatch,
} from "react-hook-form";
import { type z } from "zod";
import { episodeSchemaForm } from "./schema";
import { AddIcon, RemoveIcon } from "./icons";
import React from "react";
import TimestampInput from "./timestamp-input";

type FormValues = z.infer<typeof episodeSchemaForm>;

Expand Down Expand Up @@ -41,13 +43,17 @@ export default function Notes({
setError,
clearErrors,
watch,
setValue,
totalDuration,
}: {
control: Control<FormValues>;
register: UseFormRegister<FormValues>;
errors: FieldErrors<FormValues>;
setError: UseFormSetError<FormValues>;
watch: UseFormWatch<FormValues>;
clearErrors: UseFormClearErrors<FormValues>;
setValue: UseFormSetValue<FormValues>;
totalDuration: string;
}) {
const {
fields: noteFields,
Expand All @@ -62,10 +68,24 @@ export default function Notes({

const triggerOrderValidation = () => {
if (!notes?.length) return;
const totalDurationInSeconds = timestampToSeconds(totalDuration);

const timestamps = notes.map(note => note.timestamp);
const secondsArray = timestamps.map(timestampToSeconds);

// Check for timestamps exceeding total duration
const exceedingIndex = secondsArray.findIndex(
seconds => seconds > totalDurationInSeconds
);
if (exceedingIndex !== -1) {
setError(`notes`, {
type: "manual",
message: `Timestamp ${timestamps[exceedingIndex]} exceeds total duration of the video ${totalDuration}`,
});
return;
}

// Check timestamp order
let isSorted = true;
let firstUnsortedIndex = -1;

Expand Down Expand Up @@ -97,14 +117,13 @@ export default function Notes({
{noteFields.map((field, index) => (
<div key={field.id}>
<div className="group relative flex items-start gap-4">
<input
{...register(`notes.${index}.timestamp`, {
onChange: () => triggerOrderValidation(),
})}
placeholder="00:00:00"
pattern="^(?:[0-9]{2}:){2}[0-9]{2}$"
className="w-[100px] rounded-md border-0 bg-gray-200 px-2 py-0.5 text-sm focus:border-blue-500 focus:ring-blue-500"
title="Please use format: HH:MM:SS"
<TimestampInput
onChange={value => {
setValue(`notes.${index}.timestamp`, value);
triggerOrderValidation();
}}
defaultValue={field.timestamp}
id={`notes.${index}.timestamp`}
/>

<div className="flex-1">
Expand Down
Loading

0 comments on commit 358277e

Please sign in to comment.