feat: 0.0.1 nearly mvp
- deployed to fly using a Dockerfile 🤮
- submit button
- scroll wheel action
- only add works currently
This commit is contained in:
parent
ce759151ca
commit
1c27dd249e
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;"]
|
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.
|
||||
#
|
||||
|
||||
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
|
||||
|
|
34
mast-react-vite/package-lock.json
generated
34
mast-react-vite/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
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 { 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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -67,4 +67,15 @@
|
|||
@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