Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f8527fc4d | ||
|
|
358bfed988 |
@@ -0,0 +1,106 @@
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Row from "../../layout/Row";
|
||||
import { Info, Minus, Plus } from "lucide-react";
|
||||
import twuiNumberfy from "../../utils/numberfy";
|
||||
@@ -7,10 +7,10 @@ import { InputProps } from ".";
|
||||
let pressInterval: any;
|
||||
let pressTimeout: any;
|
||||
|
||||
type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
|
||||
type Props = Pick<InputProps<any>, "min" | "max" | "step" | "decimal"> & {
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
getNormalizedValue: (v: string) => void;
|
||||
buttonDownRef: React.MutableRefObject<boolean>;
|
||||
buttonDownRef: React.RefObject<boolean>;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
};
|
||||
|
||||
@@ -18,21 +18,50 @@ type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
|
||||
* # Input Number Text Buttons
|
||||
*/
|
||||
export default function NumberInputButtons({
|
||||
getNormalizedValue,
|
||||
value,
|
||||
setValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
buttonDownRef,
|
||||
inputRef,
|
||||
decimal,
|
||||
}: Props) {
|
||||
const PRESS_TRIGGER_TIMEOUT = 200;
|
||||
const DEFAULT_STEP = 1;
|
||||
|
||||
const [buttonDown, setButtonDown] = useState(false);
|
||||
|
||||
// function getNormalizedValue(value: string) {
|
||||
// if (numberText) {
|
||||
// if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max))
|
||||
// return getFinalValue(props.max);
|
||||
|
||||
// if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min))
|
||||
// return getFinalValue(props.min);
|
||||
|
||||
// return getFinalValue(value);
|
||||
// } else {
|
||||
// return value;
|
||||
// }
|
||||
// }
|
||||
|
||||
useEffect(() => {
|
||||
buttonDownRef.current = buttonDown;
|
||||
if (buttonDown) {
|
||||
setValue(inputRef.current?.value || "");
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setValue(inputRef.current?.value || "");
|
||||
}, 50);
|
||||
}
|
||||
}, [buttonDown]);
|
||||
|
||||
function incrementDownPress() {
|
||||
window.clearTimeout(pressTimeout);
|
||||
setButtonDown(true);
|
||||
|
||||
pressTimeout = setTimeout(() => {
|
||||
buttonDownRef.current = true;
|
||||
pressInterval = setInterval(() => {
|
||||
increment();
|
||||
}, 50);
|
||||
@@ -40,14 +69,15 @@ export default function NumberInputButtons({
|
||||
}
|
||||
|
||||
function incrementDownCancel() {
|
||||
buttonDownRef.current = false;
|
||||
setButtonDown(false);
|
||||
window.clearTimeout(pressTimeout);
|
||||
window.clearInterval(pressInterval);
|
||||
}
|
||||
|
||||
function decrementDownPress() {
|
||||
setButtonDown(true);
|
||||
|
||||
pressTimeout = setTimeout(() => {
|
||||
buttonDownRef.current = true;
|
||||
pressInterval = setInterval(() => {
|
||||
decrement();
|
||||
}, 50);
|
||||
@@ -55,41 +85,51 @@ export default function NumberInputButtons({
|
||||
}
|
||||
|
||||
function decrementDownCancel() {
|
||||
buttonDownRef.current = false;
|
||||
setButtonDown(false);
|
||||
window.clearTimeout(pressTimeout);
|
||||
window.clearInterval(pressInterval);
|
||||
}
|
||||
|
||||
function increment() {
|
||||
const existingValue = inputRef.current?.value;
|
||||
const existingNumberValue = twuiNumberfy(existingValue);
|
||||
if (!inputRef.current) return;
|
||||
|
||||
if (max && existingNumberValue >= twuiNumberfy(max)) {
|
||||
return setValue(String(max));
|
||||
} else if (min && existingNumberValue < twuiNumberfy(min)) {
|
||||
return setValue(String(min));
|
||||
const existingValue = inputRef.current.value;
|
||||
const existingNumberValue = twuiNumberfy(existingValue, decimal);
|
||||
|
||||
let new_value = "";
|
||||
|
||||
if (max && existingNumberValue >= twuiNumberfy(max, decimal)) {
|
||||
new_value = twuiNumberfy(max, decimal).toLocaleString();
|
||||
} else if (min && existingNumberValue < twuiNumberfy(min, decimal)) {
|
||||
new_value = twuiNumberfy(min, decimal).toLocaleString();
|
||||
} else {
|
||||
setValue(
|
||||
String(
|
||||
existingNumberValue + twuiNumberfy(step || DEFAULT_STEP),
|
||||
),
|
||||
);
|
||||
new_value = (
|
||||
existingNumberValue +
|
||||
twuiNumberfy(step || DEFAULT_STEP, decimal)
|
||||
).toLocaleString();
|
||||
}
|
||||
|
||||
inputRef.current.value = new_value;
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
const existingValue = inputRef.current?.value;
|
||||
const existingNumberValue = twuiNumberfy(existingValue);
|
||||
if (!inputRef.current) return;
|
||||
|
||||
if (min && existingNumberValue <= twuiNumberfy(min)) {
|
||||
setValue(String(min));
|
||||
const existingValue = inputRef.current?.value;
|
||||
const existingNumberValue = twuiNumberfy(existingValue, decimal);
|
||||
|
||||
let new_value = "";
|
||||
|
||||
if (min && existingNumberValue <= twuiNumberfy(min, decimal)) {
|
||||
new_value = twuiNumberfy(min, decimal).toLocaleString();
|
||||
} else {
|
||||
setValue(
|
||||
String(
|
||||
existingNumberValue - twuiNumberfy(step || DEFAULT_STEP),
|
||||
),
|
||||
);
|
||||
new_value = (
|
||||
existingNumberValue -
|
||||
twuiNumberfy(step || DEFAULT_STEP, decimal)
|
||||
).toLocaleString();
|
||||
}
|
||||
|
||||
inputRef.current.value = new_value;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,27 +6,21 @@ import React, {
|
||||
ReactNode,
|
||||
RefObject,
|
||||
TextareaHTMLAttributes,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Span from "../../layout/Span";
|
||||
import Button from "../../layout/Button";
|
||||
import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react";
|
||||
import { AutocompleteOptions } from "../../types";
|
||||
import twuiNumberfy from "../../utils/numberfy";
|
||||
import Dropdown from "../../elements/Dropdown";
|
||||
import Card from "../../elements/Card";
|
||||
import Stack from "../../layout/Stack";
|
||||
import NumberInputButtons from "./NumberInputButtons";
|
||||
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
|
||||
import twuiUseReady from "../../hooks/useReady";
|
||||
import Row from "../../layout/Row";
|
||||
import Paper from "../../elements/Paper";
|
||||
import { TWUISelectValidityObject } from "../Select";
|
||||
|
||||
let timeout: any;
|
||||
let validationFnTimeout: any;
|
||||
let externalValueChangeTimeout: any;
|
||||
|
||||
export type InputProps<KeyType extends string> = Omit<
|
||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
"prefix" | "suffix"
|
||||
@@ -80,6 +74,7 @@ export type InputProps<KeyType extends string> = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
// refreshDefaultValue?: number;
|
||||
};
|
||||
|
||||
let refreshes = 0;
|
||||
@@ -121,9 +116,18 @@ export default function Input<KeyType extends string>(
|
||||
validity: existingValidity,
|
||||
clearInputProps,
|
||||
rawNumber,
|
||||
// refreshDefaultValue,
|
||||
...props
|
||||
} = inputProps;
|
||||
|
||||
const componentRefreshesRef = useRef(0);
|
||||
let timeoutRef = useRef<any>(null);
|
||||
let validationFnTimeoutRef = useRef<any>(null);
|
||||
let externalValueChangeTimeoutRef = useRef<any>(null);
|
||||
|
||||
refreshes++;
|
||||
componentRefreshesRef.current++;
|
||||
|
||||
function getFinalValue(v: any) {
|
||||
if (rawNumber) return twuiNumberfy(v);
|
||||
if (numberText) {
|
||||
@@ -167,21 +171,8 @@ export default function Input<KeyType extends string>(
|
||||
props.placeholder ||
|
||||
(props.name ? twuiSlugToNormalText(props.name) : undefined);
|
||||
|
||||
function getNormalizedValue(value: string) {
|
||||
if (numberText) {
|
||||
if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max))
|
||||
return getFinalValue(props.max);
|
||||
|
||||
if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min))
|
||||
return getFinalValue(props.min);
|
||||
|
||||
return getFinalValue(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
// if (!existingReady) return;
|
||||
if (!existingValidity) return;
|
||||
setValidity(existingValidity);
|
||||
}, [existingValidity]);
|
||||
@@ -190,8 +181,8 @@ export default function Input<KeyType extends string>(
|
||||
if (buttonDownRef.current) return;
|
||||
|
||||
if (changeHandler) {
|
||||
window.clearTimeout(externalValueChangeTimeout);
|
||||
externalValueChangeTimeout = setTimeout(() => {
|
||||
window.clearTimeout(externalValueChangeTimeoutRef.current);
|
||||
externalValueChangeTimeoutRef.current = setTimeout(() => {
|
||||
changeHandler(val);
|
||||
}, finalDebounce);
|
||||
}
|
||||
@@ -208,10 +199,10 @@ export default function Input<KeyType extends string>(
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearTimeout(timeout);
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
|
||||
if (validationRegex) {
|
||||
timeout = setTimeout(() => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setValidity({
|
||||
isValid: validationRegex.test(val),
|
||||
msg: "Value mismatch",
|
||||
@@ -220,9 +211,9 @@ export default function Input<KeyType extends string>(
|
||||
}
|
||||
|
||||
if (validationFunction) {
|
||||
window.clearTimeout(validationFnTimeout);
|
||||
window.clearTimeout(validationFnTimeoutRef.current);
|
||||
|
||||
validationFnTimeout = setTimeout(() => {
|
||||
validationFnTimeoutRef.current = setTimeout(() => {
|
||||
if (validationRegex && !validationRegex.test(val)) {
|
||||
return;
|
||||
}
|
||||
@@ -235,11 +226,20 @@ export default function Input<KeyType extends string>(
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// if (!existingReady) return;
|
||||
if (typeof props.value !== "string" || !props.value.match(/./)) return;
|
||||
setValue(String(props.value));
|
||||
}, [props.value]);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (!refreshDefaultValue) return;
|
||||
// console.log("Name:", props.title || props.name);
|
||||
// console.log("props.defaultValue", props.defaultValue);
|
||||
// // setValue(String(props.defaultValue || ""));
|
||||
// }, [refreshDefaultValue]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// if (!existingReady) return;
|
||||
if (istextarea && textAreaRef.current) {
|
||||
} else if (inputRef?.current) {
|
||||
inputRef.current.value = getFinalValue(value);
|
||||
@@ -452,11 +452,12 @@ export default function Input<KeyType extends string>(
|
||||
<NumberInputButtons
|
||||
setValue={setValue}
|
||||
inputRef={inputRef}
|
||||
getNormalizedValue={getNormalizedValue}
|
||||
value={value}
|
||||
max={props.max}
|
||||
min={props.min}
|
||||
step={props.step}
|
||||
buttonDownRef={buttonDownRef}
|
||||
decimal={decimal}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
+21
-8
@@ -9,18 +9,31 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-react": "^1.7.0",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mdx": "^0.3.1",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"openai": "^6.25.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.14"
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-responsive-modal": "^7.1.0",
|
||||
"rehype-prism-plus": "^2.0.2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"next": "14.2.15",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"typescript": "^5",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@types/ace": "^0.0.52",
|
||||
"@types/bun": "latest",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^5.9.3",
|
||||
"@types/lodash": "^4.17.23"
|
||||
},
|
||||
"exports": {
|
||||
".": "./components"
|
||||
|
||||
Reference in New Issue
Block a user