diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e4b03b6 --- /dev/null +++ b/Dockerfile @@ -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;"] diff --git a/fly.toml b/fly.toml index 1006a7b..bd936cb 100644 --- a/fly.toml +++ b/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. # -app = 'mast-react-vite' +app = 'mast' primary_region = 'sea' [build] - image = 'react-server:latest' [http_service] - internal_port = 8080 + internal_port = 80 force_https = true auto_stop_machines = true auto_start_machines = true diff --git a/mast-react-vite/package-lock.json b/mast-react-vite/package-lock.json index adcbf03..982e509 100644 --- a/mast-react-vite/package-lock.json +++ b/mast-react-vite/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-table": "^8.20.5", "@vlcn.io/react": "^3.1.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", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/mast-react-vite/package.json b/mast-react-vite/package.json index 6eef330..84d36a5 100644 --- a/mast-react-vite/package.json +++ b/mast-react-vite/package.json @@ -12,6 +12,7 @@ "dependencies": { "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-table": "^8.20.5", "@vlcn.io/react": "^3.1.0", "@vlcn.io/rx-tbl": "^0.15.0", diff --git a/mast-react-vite/src/App.tsx b/mast-react-vite/src/App.tsx index 7befa6d..66e60be 100644 --- a/mast-react-vite/src/App.tsx +++ b/mast-react-vite/src/App.tsx @@ -8,28 +8,6 @@ import * as commandParser from '@/lib/command_parser.js'; import { Checkbox } from "@/components/ui/checkbox" 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 = { id: string description: string @@ -53,11 +31,11 @@ export const columns: ColumnDef[] = [ aria-label="Select all" className="translate-y-[2px]" /> - + ), cell: ({ row, table }) => (
-

{row.index+1}

+

{row.index + 1}

{ @@ -66,14 +44,14 @@ export const columns: ColumnDef[] = [ const setNewText = table.options.meta?.setNewText; const NewText = table.options.meta?.newText; if (setNewText && value === true) { - const calculatedRowId = row.index+1 - setNewText(NewText+calculatedRowId+","); + const calculatedRowId = row.index + 1 + setNewText(NewText + calculatedRowId + ","); } else { - // TODO: - // We need to _remove_ the value here - // That probably means taking the NewText - // Removing the first instance of "row.index," - // And then setting that with setNewText + // TODO: + // We need to _remove_ the value here + // That probably means taking the NewText + // Removing the first instance of "row.index," + // And then setting that with setNewText } }} aria-label="Select row" @@ -107,53 +85,40 @@ function App({ ctx }) { const todos = useQuery(ctx, "SELECT * FROM todos where completed = 0").data; const [newText, setNewText] = useState(""); const [rowSelection, setRowSelection] = useState({}); + const [currentAction, setCurrentAction] = useState("add"); - const addTodo = (e) => { - if (e.key === "Enter" && e.target.value.trim() !== "") { - 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 handleActionChange = (action: string) => { + setCurrentAction(action); + }; - const parseTodos = (e) => { - // On enter execute the command - if (e.key === "Enter") { - console.log("enter confirmed") - try { - const parsed = commandParser.parse(e.target.value); - // TODO: - // We need to identify the type of the command first - // Currently assumes that it is done - switch (parsed.type) { - case "done": - 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); + const executeCommand = (value: string) => { + try { + console.log(value) + const parsed = commandParser.parse(value); + // TODO: + // We need to identify the type of the command first + // Currently assumes that it is done + switch (parsed.type) { + case "done": + if (parsed.filters && parsed.filters.length > 0) { + const idFilters = parsed.filters.filter(f => f.type === "id"); - // Execute SQL update for each row - if (rowIds.length > 0) { - rowIds.forEach(id => { - ctx.db.exec( - `UPDATE todos SET completed = 1 WHERE id = ?`, - [id] - ); - }); - } + // Get the row IDs from the table + const rowIds = idFilters.flatMap(filter => + filter.ids.map(id => todos[id - 1]?.id) + ).filter(Boolean); - setNewText(""); - setRowSelection({}); + // Execute SQL update for each row + if (rowIds.length > 0) { + rowIds.forEach(id => { + ctx.db.exec( + `UPDATE todos SET completed = 1 WHERE id = ?`, + [id] + ); + }); } + break; + } case "add": ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed) VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`, @@ -162,15 +127,26 @@ function App({ ctx }) { parsed.tags, parsed.project ]); - setNewText(""); - } - } catch (error) { - // TODO: - // This is actually bad - // We want to throw an error to the user here - console.log("Unable to parse field"); - return; + break; } + 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 // TODO: @@ -179,30 +155,30 @@ function App({ ctx }) { // Rather than changes to the input value else if (e.target.value.trim() !== "") { try { - const parsed = commandParser.parse(e.target.value); + const parsed = commandParser.parse(currentAction + " " + e.target.value); // TODO: // We want to reactively update based on command still // Right now this assumes the command is filter - + if (parsed.filters && parsed.filters.length > 0) { const idFilters = parsed.filters.filter(f => f.type === "id"); - + // TODO: // We should probably have selection state react to changes on NewText // Rather than manually updating here // Create new selection state const newSelection = {}; - idFilters.forEach(filter => { - filter.ids.forEach(id => { - newSelection[id-1] = true; - }) - }); - + idFilters.forEach(filter => { + filter.ids.forEach(id => { + newSelection[id - 1] = true; + }) + }); + setRowSelection(newSelection); } } catch (error) { - console.log("Unable to parse field") + console.log("Unable to parse field onUpdate") return; } } else { @@ -213,22 +189,24 @@ function App({ ctx }) { return ( <>
- setNewText(e.target.value)} - /> -
- + setNewText(e.target.value)} + onSubmit={executeCommand} + /> +
+
) diff --git a/mast-react-vite/src/components/ui/button.tsx b/mast-react-vite/src/components/ui/button.tsx new file mode 100644 index 0000000..65d4fcd --- /dev/null +++ b/mast-react-vite/src/components/ui/button.tsx @@ -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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/mast-react-vite/src/components/ui/input.tsx b/mast-react-vite/src/components/ui/input.tsx index 7b04f01..2f37962 100644 --- a/mast-react-vite/src/components/ui/input.tsx +++ b/mast-react-vite/src/components/ui/input.tsx @@ -1,38 +1,52 @@ import * as React from "react" import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +// Add onActionChange to InputProps interface export interface InputProps - extends React.InputHTMLAttributes {} + extends React.InputHTMLAttributes { + onActionChange?: (action: string) => void; + onSubmit?: (value: string) => void; // Add this prop +} const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - const [selectedIndex, setSelectedIndex] = React.useState(0) - const options = ["add", "done", "delete"] - - const handleWheel = (event: React.WheelEvent) => { + ({ className, type, onActionChange, onSubmit, value, ...props }, ref) => { + const [selectedIndex, setSelectedIndex] = React.useState(0) + const options = ["add", "filter", "done", "delete"] + + const handleWheel = (event: React.WheelEvent) => { event.preventDefault() if (event.deltaY > 0) { - // Scroll down - setSelectedIndex((prev) => (prev + 1) % options.length) + const newIndex = (selectedIndex + 1) % options.length + setSelectedIndex(newIndex) + onActionChange?.(options[newIndex]) } else { - // Scroll up - setSelectedIndex((prev) => (prev - 1 + options.length) % options.length) + const newIndex = (selectedIndex - 1 + options.length) % options.length + setSelectedIndex(newIndex) + onActionChange?.(options[newIndex]) } } - + + const handleSubmit = () => { + if (onSubmit && value) { + console.log("trigger button") + onSubmit(options[selectedIndex] + " " + value); + } + } + return ( -
- { - setSelectedIndex(options.indexOf(e.target.value)) + const newIndex = options.indexOf(e.target.value) + setSelectedIndex(newIndex) + onActionChange?.(e.target.value) }} >
) } diff --git a/mast-react-vite/src/index.css b/mast-react-vite/src/index.css index 1ad6a27..e6773a3 100644 --- a/mast-react-vite/src/index.css +++ b/mast-react-vite/src/index.css @@ -67,4 +67,15 @@ @apply bg-background text-foreground; } } + + + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +}