Compare commits
3 commits
0344976747
...
1c27dd249e
Author | SHA1 | Date | |
---|---|---|---|
|
1c27dd249e | ||
|
ce759151ca | ||
|
5627318509 |
32
Dockerfile
Normal file
32
Dockerfile
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Dockerfile
|
||||||
|
|
||||||
|
# Step 1: Use a Node.js image to build the app
|
||||||
|
FROM node:18 as builder
|
||||||
|
|
||||||
|
# Set working directory inside the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package.json and package-lock.json
|
||||||
|
COPY ./mast-react-vite/package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the app’s source code
|
||||||
|
COPY ./mast-react-vite/ .
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# Step 2: Use an Nginx image to serve the static files
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy the build files from the builder stage to the Nginx web directory
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start Nginx server
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
|
@ -69,7 +69,7 @@
|
||||||
paths = [
|
paths = [
|
||||||
pkgs.nginx
|
pkgs.nginx
|
||||||
react
|
react
|
||||||
(pkgs.writeScriptBin "start-server" ''
|
(pkgs.writeScriptBin "start-server.sh" ''
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
|
@ -88,8 +88,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
Cmd = [ "/bin/start-server" ];
|
Cmd = [ "/start-server.sh" ];
|
||||||
ExposedPorts = { "8080/tcp" = { }; };
|
ExposedPorts = { "8080/tcp" = { }; };
|
||||||
|
Entrypoint = ["/start-server.sh"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
7
fly.toml
7
fly.toml
|
@ -1,16 +1,15 @@
|
||||||
# fly.toml app configuration file generated for mast-react-vite-cold-snowflake-8783 on 2025-02-19T23:28:02-08:00
|
# fly.toml app configuration file generated for mast on 2025-02-23T00:18:31-08:00
|
||||||
#
|
#
|
||||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||||
#
|
#
|
||||||
|
|
||||||
app = 'mast-react-vite'
|
app = 'mast'
|
||||||
primary_region = 'sea'
|
primary_region = 'sea'
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
image = 'react-server:latest'
|
|
||||||
|
|
||||||
[http_service]
|
[http_service]
|
||||||
internal_port = 8080
|
internal_port = 80
|
||||||
force_https = true
|
force_https = true
|
||||||
auto_stop_machines = true
|
auto_stop_machines = true
|
||||||
auto_start_machines = true
|
auto_start_machines = true
|
||||||
|
|
34
mast-react-vite/package-lock.json
generated
34
mast-react-vite/package-lock.json
generated
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@vlcn.io/react": "^3.1.0",
|
"@vlcn.io/react": "^3.1.0",
|
||||||
"@vlcn.io/rx-tbl": "^0.15.0",
|
"@vlcn.io/rx-tbl": "^0.15.0",
|
||||||
|
@ -809,7 +810,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||||
|
@ -826,6 +827,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@vlcn.io/react": "^3.1.0",
|
"@vlcn.io/react": "^3.1.0",
|
||||||
"@vlcn.io/rx-tbl": "^0.15.0",
|
"@vlcn.io/rx-tbl": "^0.15.0",
|
||||||
|
|
|
@ -8,28 +8,6 @@ import * as commandParser from '@/lib/command_parser.js';
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
interface MarshalledCommand {
|
|
||||||
description: string;
|
|
||||||
tags: string;
|
|
||||||
project: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Marshall(parsed): MarshalledCommand {
|
|
||||||
let project = "";
|
|
||||||
const tags = [];
|
|
||||||
for (const attr of parsed.attributes) {
|
|
||||||
switch (attr.type) {
|
|
||||||
case 'project':
|
|
||||||
project = attr.value;
|
|
||||||
break;
|
|
||||||
case 'tag':
|
|
||||||
tags.push(attr.value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { description: parsed.description, tags: JSON.stringify(tags), project: project };
|
|
||||||
}
|
|
||||||
|
|
||||||
type Todo = {
|
type Todo = {
|
||||||
id: string
|
id: string
|
||||||
description: string
|
description: string
|
||||||
|
@ -53,11 +31,11 @@ export const columns: ColumnDef<Todo>[] = [
|
||||||
aria-label="Select all"
|
aria-label="Select all"
|
||||||
className="translate-y-[2px]"
|
className="translate-y-[2px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row, table }) => (
|
cell: ({ row, table }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p>{row.index+1}</p>
|
<p>{row.index + 1}</p>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={row.getIsSelected()}
|
checked={row.getIsSelected()}
|
||||||
onCheckedChange={(value) => {
|
onCheckedChange={(value) => {
|
||||||
|
@ -66,14 +44,14 @@ export const columns: ColumnDef<Todo>[] = [
|
||||||
const setNewText = table.options.meta?.setNewText;
|
const setNewText = table.options.meta?.setNewText;
|
||||||
const NewText = table.options.meta?.newText;
|
const NewText = table.options.meta?.newText;
|
||||||
if (setNewText && value === true) {
|
if (setNewText && value === true) {
|
||||||
const calculatedRowId = row.index+1
|
const calculatedRowId = row.index + 1
|
||||||
setNewText(NewText+calculatedRowId+",");
|
setNewText(NewText + calculatedRowId + ",");
|
||||||
} else {
|
} else {
|
||||||
// TODO:
|
// TODO:
|
||||||
// We need to _remove_ the value here
|
// We need to _remove_ the value here
|
||||||
// That probably means taking the NewText
|
// That probably means taking the NewText
|
||||||
// Removing the first instance of "row.index,"
|
// Removing the first instance of "row.index,"
|
||||||
// And then setting that with setNewText
|
// And then setting that with setNewText
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label="Select row"
|
aria-label="Select row"
|
||||||
|
@ -107,53 +85,40 @@ function App({ ctx }) {
|
||||||
const todos = useQuery(ctx, "SELECT * FROM todos where completed = 0").data;
|
const todos = useQuery(ctx, "SELECT * FROM todos where completed = 0").data;
|
||||||
const [newText, setNewText] = useState("");
|
const [newText, setNewText] = useState("");
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
|
const [currentAction, setCurrentAction] = useState("add");
|
||||||
|
|
||||||
const addTodo = (e) => {
|
const handleActionChange = (action: string) => {
|
||||||
if (e.key === "Enter" && e.target.value.trim() !== "") {
|
setCurrentAction(action);
|
||||||
const parsed = Marshall(addParser.parse(e.target.value));
|
};
|
||||||
ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed)
|
|
||||||
VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`,
|
|
||||||
[
|
|
||||||
parsed.description,
|
|
||||||
parsed.tags,
|
|
||||||
parsed.project
|
|
||||||
]);
|
|
||||||
setNewText("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseTodos = (e) => {
|
const executeCommand = (value: string) => {
|
||||||
// On enter execute the command
|
try {
|
||||||
if (e.key === "Enter") {
|
console.log(value)
|
||||||
console.log("enter confirmed")
|
const parsed = commandParser.parse(value);
|
||||||
try {
|
// TODO:
|
||||||
const parsed = commandParser.parse(e.target.value);
|
// We need to identify the type of the command first
|
||||||
// TODO:
|
// Currently assumes that it is done
|
||||||
// We need to identify the type of the command first
|
switch (parsed.type) {
|
||||||
// Currently assumes that it is done
|
case "done":
|
||||||
switch (parsed.type) {
|
if (parsed.filters && parsed.filters.length > 0) {
|
||||||
case "done":
|
const idFilters = parsed.filters.filter(f => f.type === "id");
|
||||||
if (parsed.filters && parsed.filters.length > 0) {
|
|
||||||
const idFilters = parsed.filters.filter(f => f.type === "id");
|
|
||||||
|
|
||||||
// Get the row IDs from the table
|
|
||||||
const rowIds = idFilters.flatMap(filter =>
|
|
||||||
filter.ids.map(id => todos[id - 1]?.id)
|
|
||||||
).filter(Boolean);
|
|
||||||
|
|
||||||
// Execute SQL update for each row
|
// Get the row IDs from the table
|
||||||
if (rowIds.length > 0) {
|
const rowIds = idFilters.flatMap(filter =>
|
||||||
rowIds.forEach(id => {
|
filter.ids.map(id => todos[id - 1]?.id)
|
||||||
ctx.db.exec(
|
).filter(Boolean);
|
||||||
`UPDATE todos SET completed = 1 WHERE id = ?`,
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewText("");
|
// Execute SQL update for each row
|
||||||
setRowSelection({});
|
if (rowIds.length > 0) {
|
||||||
|
rowIds.forEach(id => {
|
||||||
|
ctx.db.exec(
|
||||||
|
`UPDATE todos SET completed = 1 WHERE id = ?`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "add":
|
case "add":
|
||||||
ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed)
|
ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed)
|
||||||
VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`,
|
VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`,
|
||||||
|
@ -162,15 +127,26 @@ function App({ ctx }) {
|
||||||
parsed.tags,
|
parsed.tags,
|
||||||
parsed.project
|
parsed.project
|
||||||
]);
|
]);
|
||||||
setNewText("");
|
break;
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// TODO:
|
|
||||||
// This is actually bad
|
|
||||||
// We want to throw an error to the user here
|
|
||||||
console.log("Unable to parse field");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
console.log("Why won't you clear?!")
|
||||||
|
setNewText("");
|
||||||
|
setRowSelection({});
|
||||||
|
} catch (error) {
|
||||||
|
// TODO:
|
||||||
|
// This is actually bad
|
||||||
|
// We want to throw an error to the user here
|
||||||
|
console.log("Unable to parse field onExecute");
|
||||||
|
console.log(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseTodos = (e) => {
|
||||||
|
// On enter execute the command
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
console.log("enter confirmed")
|
||||||
|
executeCommand(currentAction + " " + e.target.value);
|
||||||
}
|
}
|
||||||
// React to key presses for selection
|
// React to key presses for selection
|
||||||
// TODO:
|
// TODO:
|
||||||
|
@ -179,30 +155,30 @@ function App({ ctx }) {
|
||||||
// Rather than changes to the input value
|
// Rather than changes to the input value
|
||||||
else if (e.target.value.trim() !== "") {
|
else if (e.target.value.trim() !== "") {
|
||||||
try {
|
try {
|
||||||
const parsed = commandParser.parse(e.target.value);
|
const parsed = commandParser.parse(currentAction + " " + e.target.value);
|
||||||
// TODO:
|
// TODO:
|
||||||
// We want to reactively update based on command still
|
// We want to reactively update based on command still
|
||||||
// Right now this assumes the command is filter
|
// Right now this assumes the command is filter
|
||||||
|
|
||||||
if (parsed.filters && parsed.filters.length > 0) {
|
if (parsed.filters && parsed.filters.length > 0) {
|
||||||
const idFilters = parsed.filters.filter(f => f.type === "id");
|
const idFilters = parsed.filters.filter(f => f.type === "id");
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// We should probably have selection state react to changes on NewText
|
// We should probably have selection state react to changes on NewText
|
||||||
// Rather than manually updating here
|
// Rather than manually updating here
|
||||||
|
|
||||||
// Create new selection state
|
// Create new selection state
|
||||||
const newSelection = {};
|
const newSelection = {};
|
||||||
idFilters.forEach(filter => {
|
idFilters.forEach(filter => {
|
||||||
filter.ids.forEach(id => {
|
filter.ids.forEach(id => {
|
||||||
newSelection[id-1] = true;
|
newSelection[id - 1] = true;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
setRowSelection(newSelection);
|
setRowSelection(newSelection);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Unable to parse field")
|
console.log("Unable to parse field onUpdate")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -213,22 +189,24 @@ function App({ ctx }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="pt-8 container">
|
<section className="pt-8 container">
|
||||||
<Input type="text"
|
<Input type="text"
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
value={newText}
|
value={newText}
|
||||||
onKeyUp={parseTodos}
|
onKeyUp={parseTodos}
|
||||||
onChange={(e) => setNewText(e.target.value)}
|
onActionChange={handleActionChange}
|
||||||
/>
|
onChange={(e) => setNewText(e.target.value)}
|
||||||
<div className="p-2"/>
|
onSubmit={executeCommand}
|
||||||
<DataTable
|
/>
|
||||||
columns={columns}
|
<div className="p-2" />
|
||||||
data={todos}
|
<DataTable
|
||||||
rowSelection={rowSelection}
|
columns={columns}
|
||||||
setRowSelection={setRowSelection}
|
data={todos}
|
||||||
setNewText={setNewText}
|
rowSelection={rowSelection}
|
||||||
newText={newText}
|
setRowSelection={setRowSelection}
|
||||||
/>
|
setNewText={setNewText}
|
||||||
|
newText={newText}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
57
mast-react-vite/src/components/ui/button.tsx
Normal file
57
mast-react-vite/src/components/ui/button.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
|
@ -1,38 +1,52 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
// Add onActionChange to InputProps interface
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
onActionChange?: (action: string) => void;
|
||||||
|
onSubmit?: (value: string) => void; // Add this prop
|
||||||
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, onActionChange, onSubmit, value, ...props }, ref) => {
|
||||||
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
||||||
const options = ["add", "done", "delete"]
|
const options = ["add", "filter", "done", "delete"]
|
||||||
|
|
||||||
const handleWheel = (event: React.WheelEvent) => {
|
const handleWheel = (event: React.WheelEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (event.deltaY > 0) {
|
if (event.deltaY > 0) {
|
||||||
// Scroll down
|
const newIndex = (selectedIndex + 1) % options.length
|
||||||
setSelectedIndex((prev) => (prev + 1) % options.length)
|
setSelectedIndex(newIndex)
|
||||||
|
onActionChange?.(options[newIndex])
|
||||||
} else {
|
} else {
|
||||||
// Scroll up
|
const newIndex = (selectedIndex - 1 + options.length) % options.length
|
||||||
setSelectedIndex((prev) => (prev - 1 + options.length) % options.length)
|
setSelectedIndex(newIndex)
|
||||||
|
onActionChange?.(options[newIndex])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (onSubmit && value) {
|
||||||
|
console.log("trigger button")
|
||||||
|
onSubmit(options[selectedIndex] + " " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative"
|
<div className="relative items-center" onWheel={handleWheel}>
|
||||||
onWheel={handleWheel}
|
<select
|
||||||
>
|
|
||||||
<select
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-0 left-0 h-9 rounded-l-md border-r border-input bg-transparent px-2 text-sm cursor-default",
|
"absolute top-0 left-0 h-9 rounded-l-md border-r border-input bg-transparent px-2 text-sm cursor-default",
|
||||||
"shadow-sm transition-all duration-200 text-center appearance-none",
|
"shadow-sm transition-all duration-200 text-center appearance-none",
|
||||||
)}
|
)}
|
||||||
value={options[selectedIndex]}
|
value={options[selectedIndex]}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedIndex(options.indexOf(e.target.value))
|
const newIndex = options.indexOf(e.target.value)
|
||||||
|
setSelectedIndex(newIndex)
|
||||||
|
onActionChange?.(e.target.value)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="" disabled selected>
|
<option value="" disabled selected>
|
||||||
|
@ -47,12 +61,21 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-background px-[90px] text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex-1 h-9 w-full rounded-md border border-input bg-background px-[90px] text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
value={value}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 z-10"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,4 +67,15 @@
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue