Skip to content

Commit

Permalink
Merge pull request #22 from lumastic/drew-paste-lyrics
Browse files Browse the repository at this point in the history
Pasting lyrics from outside sources
  • Loading branch information
drewlyton authored Feb 12, 2023
2 parents 1decbd8 + 8e9a5b4 commit e100e6b
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 34 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ node_modules
/prisma/data.db-journal
/postgres-data

tailwind.css

.DS_Store
coverage
build
12 changes: 6 additions & 6 deletions app/components/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export const NavBar: React.FC<Props> = () => {
className={({ isActive }) =>
`${
isActive
? "border-primary-500 font-medium text-primary-600"
: "text-neutral-500 hover:text-primary-600 hover:opacity-80"
} border-b-2 border-transparent text-sm uppercase transition-all`
? " border-primary-500 font-medium text-primary-600"
: "border-transparent text-neutral-500 hover:text-primary-600 hover:opacity-80"
} border-b-2 text-sm uppercase transition-all`
}
>
Songs
Expand All @@ -38,9 +38,9 @@ export const NavBar: React.FC<Props> = () => {
className={({ isActive }) =>
`${
isActive
? "border-primary-500 font-medium text-primary-600"
: "text-neutral-500 hover:text-primary-600 hover:opacity-80"
} border-b-2 border-transparent text-sm uppercase transition-all`
? " border-primary-500 font-medium text-primary-600"
: "border-transparent text-neutral-500 hover:text-primary-600 hover:opacity-80"
} border-b-2 text-sm uppercase transition-all`
}
>
Setlists
Expand Down
57 changes: 44 additions & 13 deletions app/components/forms/song/LineFields/LineFields.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ILine, IMarking } from "@/types/song";
import { Menu } from "@headlessui/react";
import type { KeyboardEvent } from "react";
import type { ClipboardEvent, KeyboardEvent } from "react";
import { useState } from "react";
import { usePopper } from "react-popper";
import { Input } from "~/components/Input";
Expand All @@ -9,17 +9,20 @@ import { Fieldset, useNamespace } from "~/lib/fieldset";
import { useArray } from "~/lib/useArray";
import { useFocus } from "~/lib/useFocus";
import uniqid from "uniqid";
import { convertPlainTextToLines } from "~/helpers/convertPlainTextToLines";

type Props = {
line: ILine;
insertLine?: () => void;
insertLine?: (lyric?: string) => void;
updateLine?: (lyric: string) => void;
deleteLine?: () => void;
};

export const LineFields: React.FC<Props> = ({
line,
insertLine,
deleteLine,
updateLine,
}) => {
const {
items: markingsFieldArray,
Expand All @@ -44,41 +47,68 @@ export const LineFields: React.FC<Props> = ({
placement: "left",
});

const insertMark = (index: number) => {
function insertMark(index: number) {
return () => {
const length = markingsFieldArray.length;
push({ indent: index, id: uniqid(), mark: "" } as IMarking);
focus(`input[name="${namespace}markings[${length}].mark"]`);
};
};
}

const removeMark = (index: number) => {
function removeMark(index: number) {
return () => {
remove(index);
};
};
}

const newLineOnEnterKey = (e: KeyboardEvent<HTMLInputElement>) => {
function newLineOnEnterKey(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
e.preventDefault();
if (insertLine) insertLine();
const target = e.target as HTMLInputElement;
// Get text after the current cursor position
const afterCursorText = target.value.slice(
target.selectionStart ?? undefined
);
// Remove the text after the cursor from current lyrics input
target.value = target.value.slice(0, target.selectionStart ?? undefined);
// Insert line with text after the cursor
if (insertLine) insertLine(afterCursorText);
}
};
}

const confirmAndDelete = () => {
async function onPasteLyrics(e: ClipboardEvent<HTMLInputElement>) {
const pastedText = e.clipboardData.getData("text/plain");
const target = e.target as HTMLInputElement;
// If input is blank and pasted text includes an end return
if (!target.value && pastedText.includes("\n")) {
// Prevent default paste
e.preventDefault();
const linesToAdd = convertPlainTextToLines(pastedText);
// Reverse the array for adding it to the lyrics linearly
linesToAdd.reverse();
if (updateLine) updateLine(linesToAdd.pop()?.lyrics || "");
// Insert the new lyrics in reverse order
linesToAdd.forEach((line) => {
if (insertLine) insertLine(line.lyrics);
});
}

// Trigger handlePaste from StanzaContext
}

function confirmAndDelete() {
if (
confirm(
"Are you sure you want to delete this line?\nYou can't undo this action."
)
) {
if (deleteLine) deleteLine();
}
};
}

return (
<div className="w-full whitespace-nowrap" data-testid="line-fields">
<Input name="id" hidden readOnly defaultValue={line.id} />

<div className="group relative" ref={setReferenceElement}>
<Menu>
<div
Expand All @@ -105,7 +135,7 @@ export const LineFields: React.FC<Props> = ({
</svg>
</Menu.Button>
</div>
<Menu.Items className="divide-gray-100 absolute z-10 w-28 origin-top-left -translate-x-9 divide-y rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Menu.Items className="absolute z-20 w-28 origin-top-left -translate-x-9 divide-y divide-neutral-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Menu.Item>
{({ active }) => (
<button
Expand Down Expand Up @@ -166,6 +196,7 @@ export const LineFields: React.FC<Props> = ({
className="font-mono"
defaultValue={line.lyrics}
onKeyDown={newLineOnEnterKey}
onPaste={onPasteLyrics}
/>
<Input
name="notes"
Expand Down
19 changes: 14 additions & 5 deletions app/components/forms/song/StanzaFields/StanzaFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ export const StanzaFields: React.FC<{
insertStanza?: () => void;
deleteStanza?: () => void;
}> = ({ stanza, insertStanza, deleteStanza }) => {
const { items: lines, insert, remove } = useArray(stanza.lines);
const { items: lines, insert, remove, update } = useArray(stanza.lines);

const focus = useFocus();
const namespace = useNamespace("");

const insertLine = (index: number) => {
return () => {
insert(index + 1, createMockStanza({}).lines[0]);
return (lyrics?: string) => {
const lineToInsert = createMockStanza({}).lines[0];
lineToInsert.lyrics = lyrics || "";
insert(index + 1, lineToInsert);
focus(`input[name="${namespace}lines[${index + 1}].lyrics"]`);
};
};
Expand All @@ -31,6 +33,12 @@ export const StanzaFields: React.FC<{
};
};

const updateLine = (index: number) => {
return (lyrics: string) => {
update(index, { lyrics });
};
};

const confirmAndDelete = () => {
if (
confirm(
Expand Down Expand Up @@ -76,14 +84,15 @@ export const StanzaFields: React.FC<{
<Fieldset.Headless namespace={`lines[${index}]`}>
<LineFields
line={line}
updateLine={updateLine(index)}
deleteLine={deleteLine(index)}
insertLine={insertLine(index)}
/>
</Fieldset.Headless>
<button
type="button"
aria-label="Add Line"
onClick={insertLine(index)}
onClick={() => insertLine(index)()}
className="block h-2 w-full rounded-sm bg-neutral-200 py-1 font-mono text-sm leading-none text-neutral-600 opacity-0 transition-all hover:h-6 hover:opacity-100"
>
+ Add Line
Expand All @@ -94,7 +103,7 @@ export const StanzaFields: React.FC<{
<button
type="button"
aria-label="Add Line"
onClick={insertLine(0)}
onClick={() => insertLine(0)()}
className="block h-2 w-full rounded-sm bg-neutral-200 py-1 font-mono text-sm leading-none text-neutral-600 opacity-0 transition-all hover:h-6 hover:opacity-100"
>
+ Add Line
Expand Down
32 changes: 32 additions & 0 deletions app/helpers/convertPlainTextToLines.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ILine } from "@/types/song";
import { convertPlainTextToLines } from "./convertPlainTextToLines";

const samplePastedText =
"This is a song\nWith an single stanza\nThis is a song";

const singleStanzaConverted: ILine[] = [
{
id: expect.any(String),
lyrics: "This is a song",
markings: [],
notes: "",
},
{
id: expect.any(String),
lyrics: "With an single stanza",
markings: [],
notes: "",
},
{
id: expect.any(String),
lyrics: "This is a song",
markings: [],
notes: "",
},
];

test("Should convert to list with single stanza", () => {
expect(convertPlainTextToLines(samplePastedText)).toMatchObject(
singleStanzaConverted
);
});
14 changes: 14 additions & 0 deletions app/helpers/convertPlainTextToLines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createMockLine } from "@/test/factories/song.factory";
import type { ILine } from "@/types/song";

export function convertPlainTextToLines(text: string) {
// Split text into Lines

const textSplitIntoLyrics = text.split("\n");
const lines = [] as ILine[];
textSplitIntoLyrics.forEach((lyrics) => {
lines.push(createMockLine({ lyrics }));
});

return lines;
}
28 changes: 28 additions & 0 deletions app/lib/useArray.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,31 @@ test("can delete items", () => {
expect(result.current.items[0]).toBe(1);
expect(result.current.items.length).toBe(1);
});

test("can findIndex", () => {
const { result } = renderHook(() => useArray([{ id: 0 }, { id: 1 }]));

expect(result.current.findIndex((value) => value.id === 0)).toBe(0);
expect(result.current.findIndex((value) => value.id === 1)).toBe(1);

act(() => {
result.current.insert(1, { id: 3 });
});

expect(result.current.findIndex((value) => value.id === 3)).toBe(1);
});

test("can update object", () => {
const { result } = renderHook(() =>
useArray([
{ id: 0, value: 0 },
{ id: 1, value: 1 },
])
);

act(() => {
result.current.update(1, { value: 2 });
});

expect(result.current.items[1]).toMatchObject({ id: 1, value: 2 });
});
14 changes: 13 additions & 1 deletion app/lib/useArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export function useArray<T>(initialItems: T[]) {
});
};

const update = (index: number, item: Partial<T>) => {
setItems((oldItems) => {
const items = [...oldItems];
items.splice(index, 1, { ...items[index], ...item });
return items;
});
};

const push = (item: T) => {
setItems((oldItems) => {
const items = [...oldItems];
Expand All @@ -35,6 +43,10 @@ export function useArray<T>(initialItems: T[]) {
});
};

const findIndex = (predicate: (value: T) => unknown) => {
return items.findIndex(predicate);
};

const swap = (indexA: number, indexB: number) => {
setItems((oldItems) => {
const items = [...oldItems];
Expand All @@ -43,5 +55,5 @@ export function useArray<T>(initialItems: T[]) {
});
};

return { items, remove, push, replace, insert, swap };
return { items, remove, push, replace, insert, swap, findIndex, update };
}
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
neutral: colors.stone,
primary: colors.orange,
secondary: colors.cyan,
red: colors.red,
},
},
plugins: [],
Expand Down
21 changes: 12 additions & 9 deletions test/factories/song.factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMarking, IStanza } from "@/types/song";
import type { ILine, IMarking, IStanza } from "@/types/song";
import type { Song } from "@prisma/client";
import uniqid from "uniqid";

Expand Down Expand Up @@ -99,14 +99,17 @@ export function createMockStanza(p: Partial<IStanza>): IStanza {
return {
id: uniqid(),
type: "verse",
lines: [
{
id: uniqid(),
lyrics: "",
markings: [],
notes: "",
},
],
lines: [createMockLine()],
...p,
};
}

export function createMockLine(p?: Partial<ILine>): ILine {
return {
id: uniqid(),
lyrics: "",
markings: [],
notes: "",
...p,
};
}

0 comments on commit e100e6b

Please sign in to comment.