Compare commits

...

3 commits

Author SHA1 Message Date
Anish Lakhwara 1c27dd249e feat: 0.0.1 nearly mvp
- deployed to fly using a Dockerfile 🤮
- submit button
- scroll wheel action
- only add works currently
2025-02-23 17:19:15 -08:00
Anish Lakhwara" (aider) ce759151ca fix: Run start-server.sh script at container startup. 2025-02-20 18:17:14 -08:00
Anish Lakhwara 5627318509 fix: Update start-server script path in flake.nix. 2025-02-20 18:17:12 -08:00
9 changed files with 266 additions and 132 deletions

32
Dockerfile Normal file
View 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 apps 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;"]

View file

@ -69,7 +69,7 @@
paths = [
pkgs.nginx
react
(pkgs.writeScriptBin "start-server" ''
(pkgs.writeScriptBin "start-server.sh" ''
#!/usr/bin/env bash
# Create necessary directories
@ -88,8 +88,9 @@
};
config = {
Cmd = [ "/bin/start-server" ];
Cmd = [ "/start-server.sh" ];
ExposedPorts = { "8080/tcp" = { }; };
Entrypoint = ["/start-server.sh"];
};
};
};

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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<Todo>[] = [
aria-label="Select all"
className="translate-y-[2px]"
/>
</div>
</div>
),
cell: ({ row, table }) => (
<div className="flex items-center gap-2">
<p>{row.index+1}</p>
<p>{row.index + 1}</p>
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => {
@ -66,14 +44,14 @@ export const columns: ColumnDef<Todo>[] = [
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 (
<>
<section className="pt-8 container">
<Input type="text"
className="bg-background"
placeholder="..."
value={newText}
onKeyUp={parseTodos}
onChange={(e) => setNewText(e.target.value)}
/>
<div className="p-2"/>
<DataTable
columns={columns}
data={todos}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
setNewText={setNewText}
newText={newText}
/>
<Input type="text"
className="bg-background"
placeholder="..."
value={newText}
onKeyUp={parseTodos}
onActionChange={handleActionChange}
onChange={(e) => setNewText(e.target.value)}
onSubmit={executeCommand}
/>
<div className="p-2" />
<DataTable
columns={columns}
data={todos}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
setNewText={setNewText}
newText={newText}
/>
</section>
</>
)

View 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 }

View file

@ -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<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {
onActionChange?: (action: string) => void;
onSubmit?: (value: string) => void; // Add this prop
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ 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 (
<div className="relative"
onWheel={handleWheel}
>
<select
<div className="relative items-center" onWheel={handleWheel}>
<select
className={cn(
"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]}
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>
@ -47,12 +61,21 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
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
)}
ref={ref}
value={value}
{...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>
)
}

View file

@ -67,4 +67,15 @@
@apply bg-background text-foreground;
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}