233 lines
8.7 KiB
TypeScript
233 lines
8.7 KiB
TypeScript
"use client";
|
|
import { Snippet } from "@nextui-org/snippet";
|
|
import { Code } from "@nextui-org/code";
|
|
import { Icon } from "@iconify/react";
|
|
import {
|
|
Button,
|
|
ScrollShadow,
|
|
Select,
|
|
SelectItem,
|
|
} from "@nextui-org/react";
|
|
import { cn } from "@nextui-org/theme";
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
import { title } from "@/components/primitives";
|
|
import { ChapterItem, Chapters, readFile, splitChapter } from "./utils/chapter";
|
|
import { ChapterCard } from "@/components/ChapterCard";
|
|
|
|
export default function Home() {
|
|
|
|
const [text,setText] = useState("")
|
|
const [chapters,setChapters] = useState<Chapters>([])
|
|
const [activeChapter,setActiveChapter] = useState<number|null>(null)
|
|
const textareaRef = useRef<HTMLTextAreaElement|null>(null)
|
|
|
|
const onSplit = useCallback(async ()=>{
|
|
const dom = textareaRef.current
|
|
if(!dom) return
|
|
const index = dom.selectionStart
|
|
if(index<0) return
|
|
console.log("bookmark",index)
|
|
const content = '====SPLIT CHAPTER===='
|
|
setText(txt=>{
|
|
return txt.substring(0,index)+'\n'+content+"\n"+txt.substring(15)
|
|
})
|
|
},[])
|
|
|
|
const onMergeNext = useCallback((chapter:ChapterItem)=>{
|
|
const current = chapters.findIndex(detail=>detail.key===chapter.key)
|
|
if(current==-1) return
|
|
const next = current+1
|
|
const nextChapter = chapters[next]
|
|
if(!nextChapter) return
|
|
chapter.text += ["",nextChapter.title,nextChapter.text].join("\n")
|
|
const newChapters = chapters.slice()
|
|
newChapters.splice(next,1)
|
|
const newText = newChapters.reduce<string>((txt,chapter)=>{
|
|
txt += chapter.rawTitle+'\n'
|
|
txt += chapter.text+"\n"
|
|
return txt
|
|
},"")
|
|
setText(newText)
|
|
|
|
},[chapters])
|
|
|
|
const selectFile = useCallback(()=>{
|
|
const input = document.createElement("input")
|
|
input.type = 'file'
|
|
input.accept = '.txt'
|
|
input.addEventListener("change",async function(event){
|
|
setActiveChapter(null)
|
|
const [file] = input.files||[]
|
|
if(!file) {
|
|
setText("")
|
|
return
|
|
}
|
|
console.time("read-txt")
|
|
setText(await readFile(file))
|
|
console.timeEnd("read-txt")
|
|
|
|
})
|
|
input.click()
|
|
},[])
|
|
|
|
useEffect(()=>{
|
|
console.time("split-chapter")
|
|
const chapters = splitChapter(text)
|
|
setChapters(chapters)
|
|
if(chapters.length>0) {
|
|
setActiveChapter(1)
|
|
}
|
|
console.timeEnd("split-chapter")
|
|
},[text])
|
|
|
|
return (
|
|
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
|
{/* <div className="flex flex-row pt-6 w-48"> */}
|
|
<div className="flex flex-row pt-6 w-auto">
|
|
<Select
|
|
className="flex-auto"
|
|
items={[
|
|
{ key: "story-1", label: "story-1" },
|
|
{ key: "story-2", label: "story-2" },
|
|
{ key: "story-3", label: "story-3" },
|
|
]}
|
|
label="Story"
|
|
placeholder="Select a story"
|
|
>
|
|
{(story) => <SelectItem key={story.key}>{story.label}</SelectItem>}
|
|
</Select>
|
|
|
|
<Button className="flex-auto w-auto" onPress={selectFile}>Select</Button>
|
|
</div>
|
|
|
|
<div className="pt-6">
|
|
<div className="flex flex-row ">
|
|
<div
|
|
className={cn(
|
|
"relative flex h-full w-96 max-w-[384px] flex-1 flex-col !border-r-small border-divider pr-6 transition-[transform,opacity,margin] duration-250 ease-in-out",
|
|
)}
|
|
id="menu"
|
|
>
|
|
<header className="flex items-center text-md font-medium text-default-500 group-data-[selected=true]:text-foreground">
|
|
<Icon
|
|
className="text-default-500 mr-2"
|
|
icon="solar:clipboard-text-outline"
|
|
width={24}
|
|
/>
|
|
Chapters
|
|
</header>
|
|
<ScrollShadow
|
|
className="max-h-[calc(500px)] -mr-4"
|
|
id="menu-scroll"
|
|
>
|
|
<div className="flex flex-col gap-4 py-3 pr-4">
|
|
{
|
|
chapters.map((chapter,index)=>{
|
|
return <ChapterCard
|
|
chapter={chapter}
|
|
active={activeChapter===chapter.number}
|
|
key={`chapter-${chapter.key}`}
|
|
onPress={async ()=>{
|
|
setActiveChapter(chapter.number)
|
|
if(!textareaRef.current) return
|
|
textareaRef.current.focus()
|
|
await new Promise<void>(resolve=>{
|
|
requestAnimationFrame(()=>{
|
|
resolve()
|
|
})
|
|
})
|
|
textareaRef.current.selectionStart = chapter.indexOf
|
|
textareaRef.current.selectionEnd = chapter.indexOf
|
|
}}
|
|
onMergeNext={index===chapters.length-1?undefined:onMergeNext}
|
|
/>
|
|
})
|
|
}
|
|
</div>
|
|
</ScrollShadow>
|
|
</div>
|
|
|
|
<div className="w-full flex-1 flex-col min-w-[600px] pl-4">
|
|
<div className="flex flex-col">
|
|
<header className="flex items-center justify-between pb-2">
|
|
<div className="flex items-center gap-3">
|
|
<Button isIconOnly size="sm" variant="light">
|
|
<Icon
|
|
className="hideTooltip text-default-500"
|
|
height={24}
|
|
icon="solar:sidebar-minimalistic-outline"
|
|
width={24}
|
|
/>
|
|
</Button>
|
|
<h4 className="text-md">Chapter 1 - Chapter 1 title</h4>
|
|
</div>
|
|
</header>
|
|
<div className="w-full flex-1 flex-col min-w-[400px]">
|
|
<div className={cn("flex flex-col gap-4")}>
|
|
<div className="flex flex-col items-start">
|
|
<div className="relative mb-5 w-full h-[400px] bg-slate-50 dark:bg-gray-800 rounded-lg">
|
|
<div className="absolute inset-x-4 top-4 z-10 flex justify-between items-center">
|
|
<div className="flex justify-between">
|
|
<Button
|
|
className="mr-2 bg-white dark:bg-gray-700"
|
|
size="sm"
|
|
startContent={
|
|
<Icon
|
|
className="text-default-500"
|
|
icon="ant-design:highlight-outlined"
|
|
width={24}
|
|
/>
|
|
}
|
|
variant="flat"
|
|
>
|
|
button-1
|
|
</Button>
|
|
</div>
|
|
|
|
<Button
|
|
className="mr-2 bg-white dark:bg-gray-700"
|
|
size="sm"
|
|
startContent={
|
|
<Icon
|
|
className="text-default-500"
|
|
icon="material-symbols:save-outline"
|
|
width={24}
|
|
/>
|
|
}
|
|
variant="flat"
|
|
onPress={onSplit}
|
|
>
|
|
Split
|
|
</Button>
|
|
</div>
|
|
<div>
|
|
<ScrollShadow className="editScrollShow absolute left-2 right-2 bottom-10 top-12 text-base p-3 resize-none rounded-md border-solid border-inherit bg-slate-50 dark:bg-gray-800">
|
|
<div className="flex w-full h-full bg-slate-50 dark:bg-gray-200 rounded-lg p-2">
|
|
{/* Adjusted to use flex display for layout */}
|
|
<textarea
|
|
className="flex-1 p-3 resize-none rounded-md border border-transparent bg-slate-50 dark:bg-gray-200 text-gray-900" // Use flex-1 to allow the textarea to fill available space
|
|
value={text}
|
|
onChange={(event)=>{
|
|
setText(event.target.value)
|
|
}}
|
|
ref={textareaRef}
|
|
/>
|
|
<div className="bg-gray-100 p-1 rounded-md self-end ml-2">
|
|
{/* Added margin-left to separate from textarea, align-self to position at the bottom */}
|
|
</div>
|
|
</div>
|
|
</ScrollShadow>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|