Skip to content

Commit 7e0db32

Browse files
committed
add demo site and docs simlink
1 parent 2cb65a7 commit 7e0db32

19 files changed

+250
-60
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# Typer Diff
22

3+
![Typer Diff](assets/demo.png)

apps/docs/app/components/DiffText.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"use client";
2+
import { cn } from "@/lib/utils";
3+
import { useAtom } from "jotai";
4+
import { useRef } from "react";
5+
import { useClickInside, useClickOutside } from "@/app/hooks/hooks";
6+
import { diffText, isTyping } from "@/data/state";
7+
8+
export default function DiffText() {
9+
const [diff] = useAtom(diffText);
10+
const [typing, setTyping] = useAtom(isTyping);
11+
const ref = useRef<HTMLDivElement>(null);
12+
useClickInside(ref, () => {
13+
setTyping(true);
14+
});
15+
useClickOutside(ref, () => {
16+
setTyping(false);
17+
});
18+
return (
19+
<div
20+
className="text-3xl text-white/50 font-mono leading-relaxed tracking-wide overflow-x-hidden scroll mx-auto max-w-screen-lg"
21+
ref={ref}>
22+
{diff.diff.map((item, index) => (
23+
<span
24+
key={index}
25+
className={cn(
26+
item.type === "correct" && "text-white",
27+
item.type === "extra" &&
28+
"text-red-500/50 underline decoration-red-500/50 decoration-wavy",
29+
item.type === "missing" &&
30+
"text-white/50 underline decoration-red-500 decoration-wavy",
31+
item.type === "wrong" && "text-red-500"
32+
)}>
33+
{item.value}
34+
</span>
35+
))}
36+
</div>
37+
);
38+
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
import { allowModifications, textAtom, isTyping } from "@/data/state";
3+
import { useAtom } from "jotai";
4+
import { useEffect, useRef } from "react";
5+
6+
export default function HiddenInput() {
7+
let inputRef = useRef<HTMLInputElement>(null);
8+
let [text, setText] = useAtom(textAtom);
9+
const [allow] = useAtom(allowModifications);
10+
const [typing] = useAtom(isTyping);
11+
12+
const handleKeyDown = () => {
13+
if (!inputRef || !inputRef.current || !typing) {
14+
return;
15+
}
16+
inputRef.current.focus();
17+
let val = inputRef.current.value;
18+
inputRef.current.value = "";
19+
inputRef.current.value = val;
20+
};
21+
22+
useEffect(() => {
23+
document.addEventListener("keydown", handleKeyDown);
24+
25+
return () => {
26+
document.removeEventListener("keydown", handleKeyDown);
27+
};
28+
});
29+
30+
return (
31+
<input
32+
ref={inputRef}
33+
type="text"
34+
className="fixed -top-95 opacity-0"
35+
value={text.text}
36+
onChange={(e) => {
37+
if (allow) {
38+
setText((text) => {
39+
return { ...text, text: e.target.value };
40+
});
41+
}
42+
}}
43+
/>
44+
);
45+
}

apps/docs/app/hooks/hooks.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
3+
export const useClickInside = (
4+
ref: React.RefObject<HTMLElement>,
5+
callback: () => void
6+
) => {
7+
const handleClick = (e: MouseEvent) => {
8+
if (ref.current && ref.current.contains(e.target as Node)) {
9+
callback();
10+
}
11+
};
12+
React.useEffect(() => {
13+
document.addEventListener("click", handleClick);
14+
return () => {
15+
document.removeEventListener("click", handleClick);
16+
};
17+
});
18+
};
19+
20+
export const useClickOutside = (
21+
ref: React.RefObject<HTMLElement>,
22+
callback: () => void
23+
) => {
24+
const handleClick = (e: MouseEvent) => {
25+
if (ref.current && !ref.current.contains(e.target as Node)) {
26+
callback();
27+
}
28+
};
29+
React.useEffect(() => {
30+
document.addEventListener("click", handleClick);
31+
return () => {
32+
document.removeEventListener("click", handleClick);
33+
};
34+
});
35+
};

apps/docs/app/layout.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export default function RootLayout({
1616
}>) {
1717
return (
1818
<html lang="en">
19-
<body className={`bg-black text-white font-mono container`}>
19+
<body
20+
className={`bg-black text-white font-mono h-screen overscroll-none`}>
2021
{children}
2122
</body>
2223
</html>

apps/docs/app/page.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import DiffText from "./components/DiffText";
2+
import HiddenInput from "./components/HiddenInput";
3+
14
export default function Home() {
25
return (
3-
<main className="flex min-h-screen flex-col items-center justify-between p-24">
4-
Hello World
5-
</main>
6+
<div className="h-full leading-relaxed flex flex-col justify-center items-center">
7+
<DiffText></DiffText>
8+
<HiddenInput></HiddenInput>
9+
</div>
610
);
711
}

apps/docs/data/state.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { atom } from "jotai";
2+
import { diff } from "typer-diff";
3+
4+
let text =
5+
"typer-diff is a library to diff two strings, useful for showing for typing games (like monkeytype) or for showing differences between two strings. It is a simple library that is easy to use and has no dependencies. It is also very fast and lightweight.";
6+
export let textAtom = atom({
7+
original: text,
8+
text: "",
9+
});
10+
11+
export const diffText = atom((get) => {
12+
const text = get(textAtom);
13+
return diff(text.original, text.text);
14+
});
15+
16+
export const allowModifications = atom((get) => {
17+
const diff = get(diffText);
18+
return !diff.end;
19+
});
20+
21+
export const isTyping = atom(true);

apps/docs/docs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./apps/docs

apps/docs/lib/utils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type ClassValue, clsx } from "clsx";
2+
import { twMerge } from "tailwind-merge";
3+
4+
export function cn(...inputs: ClassValue[]) {
5+
return twMerge(clsx(inputs));
6+
}

apps/docs/next.config.mjs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/** @type {import('next').NextConfig} */
2-
const nextConfig = {};
2+
const nextConfig = {
3+
output: "export",
4+
};
35

46
export default nextConfig;

apps/docs/package.json

+28-24
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
{
2-
"name": "docs",
3-
"version": "0.1.0",
4-
"private": true,
5-
"scripts": {
6-
"dev": "next dev --turbo",
7-
"build": "next build",
8-
"start": "next start",
9-
"lint": "next lint"
10-
},
11-
"dependencies": {
12-
"react": "^18",
13-
"react-dom": "^18",
14-
"next": "14.2.4"
15-
},
16-
"devDependencies": {
17-
"typescript": "^5",
18-
"@types/node": "^20",
19-
"@types/react": "^18",
20-
"@types/react-dom": "^18",
21-
"postcss": "^8",
22-
"tailwindcss": "^3.4.1",
23-
"eslint": "^8",
24-
"eslint-config-next": "14.2.4"
25-
}
2+
"name": "docs",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev --turbo",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"clsx": "^2.1.1",
13+
"jotai": "^2.8.3",
14+
"next": "14.2.4",
15+
"react": "^18",
16+
"react-dom": "^18",
17+
"tailwind-merge": "^2.3.0",
18+
"typer-diff": "workspace:*"
19+
},
20+
"devDependencies": {
21+
"@types/node": "^20",
22+
"@types/react": "^18",
23+
"@types/react-dom": "^18",
24+
"eslint": "^8",
25+
"eslint-config-next": "14.2.4",
26+
"postcss": "^8",
27+
"tailwindcss": "^3.4.1",
28+
"typescript": "^5"
29+
}
2630
}

assets/demo.png

67.5 KB
Loading

docs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./apps/docs

packages/typer-diff/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Typer Diff
22

3-
WIP
3+
![Typer Diff Demo](demo.png)

packages/typer-diff/demo.png

67.5 KB
Loading

packages/typer-diff/index.ts

+19-24
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export const diff = (originalText: string, typedText: string): DiffResult => {
2323
}
2424
const correctWord = correctWords[i];
2525
const typedWord = typedWords[i];
26-
if (!correctWord || !typedWord) {
26+
if (!correctWord) {
2727
continue;
2828
}
29-
const wordDiff = diffWord(correctWord, typedWord);
29+
const wordDiff = diffWord(correctWord, typedWord || "");
3030
diff.push(...wordDiff);
3131
diff.push({ value: " ", type: "spacer" });
3232
}
@@ -37,10 +37,10 @@ export const diff = (originalText: string, typedText: string): DiffResult => {
3737
// compare current word
3838
const correctWord = correctWords[currentWordIndex];
3939
const typedWord = typedWords[currentWordIndex];
40-
if (!correctWord || !typedWord) {
40+
if (!correctWord) {
4141
return { diff, end: false };
4242
}
43-
const currentWordDiff = diffCurrentWord(correctWord, typedWord);
43+
const currentWordDiff = diffCurrentWord(correctWord, typedWord || "");
4444
diff.push(...currentWordDiff);
4545

4646
// words AFTER current word are untouched
@@ -71,10 +71,10 @@ const diffWord = (originalWord: string, typedWord: string) => {
7171
for (let i = 0; i < typedLength; i++) {
7272
let originalChar = originalWord[i];
7373
let typedChar = typedWord[i];
74-
if (!originalChar || !typedChar) {
74+
if (!originalChar) {
7575
continue;
7676
}
77-
diff.push(diffChar(originalChar, typedChar));
77+
diff.push(diffChar(originalChar, typedChar || ""));
7878
}
7979
// the rest are missing
8080
for (let i = typedLength; i < correctLength; i++) {
@@ -92,18 +92,16 @@ const diffWord = (originalWord: string, typedWord: string) => {
9292
for (let i = 0; i < correctLength; i++) {
9393
let originalChar = originalWord[i];
9494
let typedChar = typedWord[i];
95-
if (!originalChar || !typedChar) {
95+
if (!originalChar) {
9696
continue;
9797
}
98-
diff.push(diffChar(originalChar, typedChar));
98+
diff.push(diffChar(originalChar, typedChar || ""));
9999
}
100100
// the rest are extra
101101
for (let i = correctLength; i < typedLength; i++) {
102102
let typedChar = typedWord[i];
103-
if (!typedChar) {
104-
continue;
105-
}
106-
diff.push({ value: typedChar, type: "extra" });
103+
104+
diff.push({ value: typedChar || "", type: "extra" });
107105
}
108106
}
109107

@@ -112,10 +110,10 @@ const diffWord = (originalWord: string, typedWord: string) => {
112110
for (let i = 0; i < correctLength; i++) {
113111
let originalChar = originalWord[i];
114112
let typedChar = typedWord[i];
115-
if (!originalChar || !typedChar) {
113+
if (!originalChar) {
116114
continue;
117115
}
118-
diff.push(diffChar(originalChar, typedChar));
116+
diff.push(diffChar(originalChar, typedChar || ""));
119117
}
120118
}
121119

@@ -140,10 +138,10 @@ const diffCurrentWord = (correctWord: string, typedWord: string) => {
140138
for (let i = 0; i < typedLength; i++) {
141139
let originalChar = correctWord[i];
142140
let typedChar = typedWord[i];
143-
if (!originalChar || !typedChar) {
141+
if (!originalChar) {
144142
continue;
145143
}
146-
diff.push(diffChar(originalChar, typedChar));
144+
diff.push(diffChar(originalChar, typedChar || ""));
147145
}
148146
// the rest are untouched
149147
for (let i = typedLength; i < correctLength; i++) {
@@ -161,18 +159,15 @@ const diffCurrentWord = (correctWord: string, typedWord: string) => {
161159
for (let i = 0; i < correctLength; i++) {
162160
let originalChar = correctWord[i];
163161
let typedChar = typedWord[i];
164-
if (!originalChar || !typedChar) {
162+
if (!originalChar) {
165163
continue;
166164
}
167-
diff.push(diffChar(originalChar, typedChar));
165+
diff.push(diffChar(originalChar, typedChar || ""));
168166
}
169167
// the rest are extra
170168
for (let i = correctLength; i < typedLength; i++) {
171169
let typedChar = typedWord[i];
172-
if (!typedChar) {
173-
continue;
174-
}
175-
diff.push({ value: typedChar, type: "extra" });
170+
diff.push({ value: typedChar || "", type: "extra" });
176171
}
177172
}
178173

@@ -181,10 +176,10 @@ const diffCurrentWord = (correctWord: string, typedWord: string) => {
181176
for (let i = 0; i < correctLength; i++) {
182177
let originalChar = correctWord[i];
183178
let typedChar = typedWord[i];
184-
if (!originalChar || !typedChar) {
179+
if (!originalChar) {
185180
continue;
186181
}
187-
diff.push(diffChar(originalChar, typedChar));
182+
diff.push(diffChar(originalChar, typedChar || ""));
188183
}
189184
}
190185

packages/typer-diff/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"types": "dist/index.d.ts",
88
"scripts": {
99
"build": "tsup index.ts --format cjs,esm --dts --minify",
10+
"dev": "tsup index.ts --format cjs,esm --dts --watch",
1011
"lint": "tsc",
1112
"release": "pnpm run build && pnpm publish"
1213
},

packages/typer-diff/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"strict": true /* Enable all strict type-checking options. */,
88
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
99
"noUncheckedIndexedAccess": true,
10-
"noEmit": true
10+
"noEmit": true,
11+
"sourceMap": true
1112
}
1213
}

0 commit comments

Comments
 (0)