mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
@react.component
|
||||
let make = (
|
||||
~lesson: AnnotationTypes.lesson,
|
||||
~note: option<string>,
|
||||
~onNoteChange: (string, string) => unit,
|
||||
~onClose: unit => unit,
|
||||
) => {
|
||||
let noteValue = switch note {
|
||||
| Some(n) => n
|
||||
| None => ""
|
||||
}
|
||||
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest font-semibold">
|
||||
{React.string("Annotation")}
|
||||
</p>
|
||||
<h2 className="text-lg font-bold mt-0.5">
|
||||
{React.string(lesson.title)}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={_ => onClose()}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
||||
>
|
||||
{React.string("✕ Close")}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
className="flex-1 w-full resize-none rounded-lg border bg-background p-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder="Write your notes for this lesson..."
|
||||
value={noteValue}
|
||||
onChange={e => {
|
||||
let value = ReactEvent.Form.target(e)["value"]
|
||||
onNoteChange(lesson.id, value)
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{React.string("Auto-saved to your browser")}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
@val external localStorage: {..} = "localStorage"
|
||||
|
||||
let storageKey = (courseId: string, lessonId: string) =>
|
||||
`annotation:${courseId}:${lessonId}`
|
||||
|
||||
let saveNote = (courseId: string, lessonId: string, note: string) => {
|
||||
let key = storageKey(courseId, lessonId)
|
||||
localStorage["setItem"](key, note)
|
||||
}
|
||||
|
||||
let loadNote = (courseId: string, lessonId: string) => {
|
||||
let key = storageKey(courseId, lessonId)
|
||||
let value: Nullable.t<string> = localStorage["getItem"](key)
|
||||
Nullable.toOption(value)
|
||||
}
|
||||
|
||||
let deleteNote = (courseId: string, lessonId: string) => {
|
||||
let key = storageKey(courseId, lessonId)
|
||||
localStorage["removeItem"](key)
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
type lesson = {
|
||||
id: string,
|
||||
title: string,
|
||||
unitId: string,
|
||||
}
|
||||
|
||||
type annotationState = {
|
||||
selectedLesson: option<lesson>,
|
||||
isPenOpen: bool,
|
||||
notes: dict<string>,
|
||||
}
|
||||
|
||||
let initialState = {
|
||||
selectedLesson: None,
|
||||
isPenOpen: false,
|
||||
notes: Dict.make(),
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@react.component
|
||||
let make = () => {
|
||||
<div> {React.string("ReScript is working!")} </div>
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,198 @@
|
||||
module SketchToolbar = {
|
||||
@module("./SketchToolbar.jsx") @react.component
|
||||
external make: (
|
||||
~state: UseSketch.sketchState,
|
||||
~onSetTool: UseSketch.tool => unit,
|
||||
~onSetColor: UseSketch.color => unit,
|
||||
~onClear: unit => unit,
|
||||
~onSave: unit => unit,
|
||||
~onClose: unit => unit,
|
||||
) => React.element = "default"
|
||||
}
|
||||
|
||||
type point = {x: float, y: float}
|
||||
|
||||
@val external document: {..} = "document"
|
||||
external asCanvas: Dom.element => {..} = "%identity"
|
||||
|
||||
@react.component
|
||||
let make = (
|
||||
~isActive: bool,
|
||||
~tool: UseSketch.tool,
|
||||
~color: UseSketch.color,
|
||||
~onSave: unit => unit,
|
||||
~onClear: unit => unit,
|
||||
~onClose: unit => unit,
|
||||
~onSetTool: UseSketch.tool => unit,
|
||||
~onSetColor: UseSketch.color => unit,
|
||||
) => {
|
||||
let canvasRef: React.ref<Nullable.t<Dom.element>> = React.useRef(Nullable.null)
|
||||
let isDrawing = React.useRef(false)
|
||||
let lastPoint = React.useRef(None)
|
||||
|
||||
React.useEffect1(() => {
|
||||
switch canvasRef.current->Nullable.toOption {
|
||||
| None => ()
|
||||
| Some(el) =>
|
||||
let canvas = asCanvas(el)
|
||||
let parent = canvas["parentElement"]
|
||||
canvas["width"] = parent["offsetWidth"]
|
||||
canvas["height"] = parent["offsetHeight"]
|
||||
}
|
||||
None
|
||||
}, [isActive])
|
||||
|
||||
let getPos = (e: JsxEvent.Mouse.t, canvas) => {
|
||||
let rect = canvas["getBoundingClientRect"]()
|
||||
{
|
||||
x: Float.fromInt(JsxEvent.Mouse.clientX(e)) -. rect["left"],
|
||||
y: Float.fromInt(JsxEvent.Mouse.clientY(e)) -. rect["top"],
|
||||
}
|
||||
}
|
||||
|
||||
let startDraw = (e: JsxEvent.Mouse.t) => {
|
||||
switch canvasRef.current->Nullable.toOption {
|
||||
| None => ()
|
||||
| Some(el) =>
|
||||
let canvas = asCanvas(el)
|
||||
isDrawing.current = true
|
||||
let pos = getPos(e, canvas)
|
||||
lastPoint.current = Some(pos)
|
||||
if tool === UseSketch.Highlighter {
|
||||
let ctx = canvas["getContext"]("2d")
|
||||
let hex = UseSketch.colorToHex(color)
|
||||
let width = UseSketch.toolToWidth(tool, 2.0)
|
||||
ctx["globalAlpha"] = 0.25
|
||||
ctx["globalCompositeOperation"] = "source-over"
|
||||
ctx["strokeStyle"] = hex
|
||||
ctx["lineWidth"] = width
|
||||
ctx["lineCap"] = "round"
|
||||
ctx["lineJoin"] = "round"
|
||||
let _ = ctx["beginPath"]()
|
||||
let _ = ctx["moveTo"](pos.x, pos.y)
|
||||
} else if tool === UseSketch.Text {
|
||||
let text = canvas["ownerDocument"]["defaultView"]["prompt"]("Enter text:", "")
|
||||
switch Nullable.toOption(text) {
|
||||
| None => ()
|
||||
| Some(t) =>
|
||||
let ctx = canvas["getContext"]("2d")
|
||||
let hex = UseSketch.colorToHex(color)
|
||||
ctx["globalAlpha"] = 1.0
|
||||
ctx["fillStyle"] = hex
|
||||
ctx["font"] = "bold 16px sans-serif"
|
||||
let _ = ctx["fillText"](t, pos.x, pos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let draw = (e: JsxEvent.Mouse.t) => {
|
||||
if isDrawing.current && tool !== UseSketch.Text {
|
||||
switch canvasRef.current->Nullable.toOption {
|
||||
| None => ()
|
||||
| Some(el) =>
|
||||
let canvas = asCanvas(el)
|
||||
switch lastPoint.current {
|
||||
| None => ()
|
||||
| Some(last) =>
|
||||
let ctx = canvas["getContext"]("2d")
|
||||
let pos = getPos(e, canvas)
|
||||
let hex = UseSketch.colorToHex(color)
|
||||
let width = UseSketch.toolToWidth(tool, 2.0)
|
||||
switch tool {
|
||||
| UseSketch.Highlighter =>
|
||||
let _ = ctx["lineTo"](pos.x, pos.y)
|
||||
let _ = ctx["stroke"]()
|
||||
| _ =>
|
||||
ctx["globalAlpha"] = 1.0
|
||||
ctx["strokeStyle"] = hex
|
||||
ctx["lineWidth"] = width
|
||||
ctx["lineCap"] = "round"
|
||||
ctx["lineJoin"] = "round"
|
||||
let _ = ctx["beginPath"]()
|
||||
let _ = ctx["moveTo"](last.x, last.y)
|
||||
let _ = ctx["quadraticCurveTo"](
|
||||
last.x +. (pos.x -. last.x) /. 2.0,
|
||||
last.y +. (pos.y -. last.y) /. 2.0,
|
||||
pos.x,
|
||||
pos.y,
|
||||
)
|
||||
let _ = ctx["stroke"]()
|
||||
}
|
||||
lastPoint.current = Some(pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stopDraw = (_e: JsxEvent.Mouse.t) => {
|
||||
if tool === UseSketch.Highlighter {
|
||||
switch canvasRef.current->Nullable.toOption {
|
||||
| None => ()
|
||||
| Some(el) =>
|
||||
let canvas = asCanvas(el)
|
||||
let ctx = canvas["getContext"]("2d")
|
||||
let _ = ctx["stroke"]()
|
||||
ctx["globalAlpha"] = 1.0
|
||||
ctx["globalCompositeOperation"] = "source-over"
|
||||
}
|
||||
}
|
||||
isDrawing.current = false
|
||||
lastPoint.current = None
|
||||
}
|
||||
|
||||
let handleClear = () => {
|
||||
switch canvasRef.current->Nullable.toOption {
|
||||
| None => ()
|
||||
| Some(el) =>
|
||||
let canvas = asCanvas(el)
|
||||
let ctx = canvas["getContext"]("2d")
|
||||
let _ = ctx["clearRect"](0, 0, canvas["width"], canvas["height"])
|
||||
onClear()
|
||||
}
|
||||
}
|
||||
|
||||
let handleSave = () => {
|
||||
switch canvasRef.current->Nullable.toOption {
|
||||
| None => ()
|
||||
| Some(el) =>
|
||||
let canvas = asCanvas(el)
|
||||
let dataUrl = canvas["toDataURL"]("image/png")
|
||||
let link = document["createElement"]("a")
|
||||
link["href"] = dataUrl
|
||||
link["download"] = "sketch.png"
|
||||
let _ = link["click"]()
|
||||
onSave()
|
||||
}
|
||||
}
|
||||
|
||||
if !isActive {
|
||||
React.null
|
||||
} else {
|
||||
<div className="absolute inset-0 z-10" style={{pointerEvents: isActive ? "all" : "none"}}>
|
||||
<SketchToolbar.make
|
||||
state={{
|
||||
UseSketch.isActive,
|
||||
tool,
|
||||
color,
|
||||
strokeWidth: 2.0,
|
||||
}}
|
||||
onSetTool={onSetTool}
|
||||
onSetColor={onSetColor}
|
||||
onClear={handleClear}
|
||||
onSave={handleSave}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<canvas
|
||||
ref={canvasRef->ReactDOM.Ref.domRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="w-full h-full cursor-crosshair"
|
||||
onMouseDown={startDraw}
|
||||
onMouseMove={draw}
|
||||
onMouseUp={stopDraw}
|
||||
onMouseLeave={stopDraw}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
module SketchToolbar = {
|
||||
@module("./SketchToolbar.jsx") @react.component
|
||||
external make: (
|
||||
~state: UseSketch.sketchState,
|
||||
~onSetTool: UseSketch.tool => unit,
|
||||
~onSetColor: UseSketch.color => unit,
|
||||
~onClear: unit => unit,
|
||||
~onSave: unit => unit,
|
||||
~onClose: unit => unit,
|
||||
) => React.element = "default"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@val external window: {..} = "window"
|
||||
|
||||
let use = (courseId: string) => {
|
||||
let (state, setState) = React.useState(() => AnnotationTypes.initialState)
|
||||
|
||||
let selectLesson = (lesson: AnnotationTypes.lesson) => {
|
||||
let savedNote = AnnotationStorage.loadNote(courseId, lesson.id)
|
||||
let notes = Dict.make()
|
||||
switch savedNote {
|
||||
| Some(note) => Dict.set(notes, lesson.id, note)
|
||||
| None => ()
|
||||
}
|
||||
setState(_ => {
|
||||
selectedLesson: Some(lesson),
|
||||
isPenOpen: true,
|
||||
notes,
|
||||
})
|
||||
}
|
||||
|
||||
let updateNote = (lessonId: string, note: string) => {
|
||||
AnnotationStorage.saveNote(courseId, lessonId, note)
|
||||
setState(prev => {
|
||||
let notes = Dict.make()
|
||||
Dict.set(notes, lessonId, note)
|
||||
{...prev, notes}
|
||||
})
|
||||
}
|
||||
|
||||
let closePen = () => {
|
||||
setState(prev => {...prev, isPenOpen: false, selectedLesson: None})
|
||||
}
|
||||
|
||||
let getNote = (lessonId: string) => {
|
||||
Dict.get(state.notes, lessonId)
|
||||
}
|
||||
|
||||
(state, selectLesson, updateNote, closePen, getNote)
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
type tool =
|
||||
| Pen
|
||||
| Highlighter
|
||||
| Text
|
||||
|
||||
type color =
|
||||
| Black
|
||||
| Red
|
||||
| Blue
|
||||
| Yellow
|
||||
|
||||
type sketchState = {
|
||||
isActive: bool,
|
||||
tool: tool,
|
||||
color: color,
|
||||
strokeWidth: float,
|
||||
}
|
||||
|
||||
let initialState: sketchState = {
|
||||
isActive: false,
|
||||
tool: Pen,
|
||||
color: Black,
|
||||
strokeWidth: 2.0,
|
||||
}
|
||||
|
||||
let colorToHex = (color: color) =>
|
||||
switch color {
|
||||
| Black => "#000000"
|
||||
| Red => "#ef4444"
|
||||
| Blue => "#3b82f6"
|
||||
| Yellow => "#eab308"
|
||||
}
|
||||
|
||||
let toolToOpacity = (tool: tool) =>
|
||||
switch tool {
|
||||
| Highlighter => 0.3
|
||||
| Pen | Text => 1.0
|
||||
}
|
||||
|
||||
let isHighlighter = (tool: tool) =>
|
||||
switch tool {
|
||||
| Highlighter => true
|
||||
| Pen | Text => false
|
||||
}
|
||||
|
||||
let toolToWidth = (tool: tool, base: float) =>
|
||||
switch tool {
|
||||
| Highlighter => base *. 8.0
|
||||
| Pen => base
|
||||
| Text => base
|
||||
}
|
||||
|
||||
let use = () => {
|
||||
let (state, setState) = React.useState(() => initialState)
|
||||
|
||||
let activate = () => setState(prev => {...prev, isActive: true})
|
||||
|
||||
let deactivate = () => setState(prev => {...prev, isActive: false})
|
||||
|
||||
let toggle = () => setState(prev => {...prev, isActive: !prev.isActive})
|
||||
|
||||
let setTool = (tool: tool) => setState(prev => {...prev, tool})
|
||||
|
||||
let setColor = (color: color) => setState(prev => {...prev, color})
|
||||
|
||||
(state, activate, deactivate, toggle, setTool, setColor)
|
||||
}
|
||||
Reference in New Issue
Block a user