this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: 0.0.1 nearly mvp

- deployed to fly using a Dockerfile 🤮
- submit button
- scroll wheel action
- only add works currently

+263 -130
+32
Dockerfile
··· 1 + # Dockerfile 2 + 3 + # Step 1: Use a Node.js image to build the app 4 + FROM node:18 as builder 5 + 6 + # Set working directory inside the container 7 + WORKDIR /app 8 + 9 + # Copy package.json and package-lock.json 10 + COPY ./mast-react-vite/package*.json ./ 11 + 12 + # Install dependencies 13 + RUN npm install 14 + 15 + # Copy the rest of the app’s source code 16 + COPY ./mast-react-vite/ . 17 + 18 + # Build the app 19 + RUN npm run build 20 + 21 + 22 + # Step 2: Use an Nginx image to serve the static files 23 + FROM nginx:alpine 24 + 25 + # Copy the build files from the builder stage to the Nginx web directory 26 + COPY --from=builder /app/dist /usr/share/nginx/html 27 + 28 + # Expose port 80 29 + EXPOSE 80 30 + 31 + # Start Nginx server 32 + CMD ["nginx", "-g", "daemon off;"]
+3 -4
fly.toml
··· 1 - # fly.toml app configuration file generated for mast-react-vite-cold-snowflake-8783 on 2025-02-19T23:28:02-08:00 1 + # fly.toml app configuration file generated for mast on 2025-02-23T00:18:31-08:00 2 2 # 3 3 # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 4 # 5 5 6 - app = 'mast-react-vite' 6 + app = 'mast' 7 7 primary_region = 'sea' 8 8 9 9 [build] 10 - image = 'react-server:latest' 11 10 12 11 [http_service] 13 - internal_port = 8080 12 + internal_port = 80 14 13 force_https = true 15 14 auto_stop_machines = true 16 15 auto_start_machines = true
+33 -1
mast-react-vite/package-lock.json
··· 10 10 "dependencies": { 11 11 "@radix-ui/react-checkbox": "^1.1.2", 12 12 "@radix-ui/react-icons": "^1.3.0", 13 + "@radix-ui/react-slot": "^1.1.2", 13 14 "@tanstack/react-table": "^8.20.5", 14 15 "@vlcn.io/react": "^3.1.0", 15 16 "@vlcn.io/rx-tbl": "^0.15.0", ··· 809 810 } 810 811 } 811 812 }, 812 - "node_modules/@radix-ui/react-slot": { 813 + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { 813 814 "version": "1.1.0", 814 815 "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", 815 816 "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", 816 817 "dependencies": { 817 818 "@radix-ui/react-compose-refs": "1.1.0" 818 819 }, 820 + "peerDependencies": { 821 + "@types/react": "*", 822 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 823 + }, 824 + "peerDependenciesMeta": { 825 + "@types/react": { 826 + "optional": true 827 + } 828 + } 829 + }, 830 + "node_modules/@radix-ui/react-slot": { 831 + "version": "1.1.2", 832 + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", 833 + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", 834 + "dependencies": { 835 + "@radix-ui/react-compose-refs": "1.1.1" 836 + }, 837 + "peerDependencies": { 838 + "@types/react": "*", 839 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 840 + }, 841 + "peerDependenciesMeta": { 842 + "@types/react": { 843 + "optional": true 844 + } 845 + } 846 + }, 847 + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { 848 + "version": "1.1.1", 849 + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", 850 + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", 819 851 "peerDependencies": { 820 852 "@types/react": "*", 821 853 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+1
mast-react-vite/package.json
··· 12 12 "dependencies": { 13 13 "@radix-ui/react-checkbox": "^1.1.2", 14 14 "@radix-ui/react-icons": "^1.3.0", 15 + "@radix-ui/react-slot": "^1.1.2", 15 16 "@tanstack/react-table": "^8.20.5", 16 17 "@vlcn.io/react": "^3.1.0", 17 18 "@vlcn.io/rx-tbl": "^0.15.0",
+85 -107
mast-react-vite/src/App.tsx
··· 8 8 import { Checkbox } from "@/components/ui/checkbox" 9 9 import { Input } from "@/components/ui/input" 10 10 11 - interface MarshalledCommand { 12 - description: string; 13 - tags: string; 14 - project: string; 15 - } 16 - 17 - function Marshall(parsed): MarshalledCommand { 18 - let project = ""; 19 - const tags = []; 20 - for (const attr of parsed.attributes) { 21 - switch (attr.type) { 22 - case 'project': 23 - project = attr.value; 24 - break; 25 - case 'tag': 26 - tags.push(attr.value); 27 - break; 28 - } 29 - } 30 - return { description: parsed.description, tags: JSON.stringify(tags), project: project }; 31 - } 32 - 33 11 type Todo = { 34 12 id: string 35 13 description: string ··· 53 31 aria-label="Select all" 54 32 className="translate-y-[2px]" 55 33 /> 56 - </div> 34 + </div> 57 35 ), 58 36 cell: ({ row, table }) => ( 59 37 <div className="flex items-center gap-2"> 60 - <p>{row.index+1}</p> 38 + <p>{row.index + 1}</p> 61 39 <Checkbox 62 40 checked={row.getIsSelected()} 63 41 onCheckedChange={(value) => { ··· 66 44 const setNewText = table.options.meta?.setNewText; 67 45 const NewText = table.options.meta?.newText; 68 46 if (setNewText && value === true) { 69 - const calculatedRowId = row.index+1 70 - setNewText(NewText+calculatedRowId+","); 47 + const calculatedRowId = row.index + 1 48 + setNewText(NewText + calculatedRowId + ","); 71 49 } else { 72 - // TODO: 73 - // We need to _remove_ the value here 74 - // That probably means taking the NewText 75 - // Removing the first instance of "row.index," 76 - // And then setting that with setNewText 50 + // TODO: 51 + // We need to _remove_ the value here 52 + // That probably means taking the NewText 53 + // Removing the first instance of "row.index," 54 + // And then setting that with setNewText 77 55 } 78 56 }} 79 57 aria-label="Select row" ··· 107 85 const todos = useQuery(ctx, "SELECT * FROM todos where completed = 0").data; 108 86 const [newText, setNewText] = useState(""); 109 87 const [rowSelection, setRowSelection] = useState({}); 88 + const [currentAction, setCurrentAction] = useState("add"); 110 89 111 - const addTodo = (e) => { 112 - if (e.key === "Enter" && e.target.value.trim() !== "") { 113 - const parsed = Marshall(addParser.parse(e.target.value)); 114 - ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed) 115 - VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`, 116 - [ 117 - parsed.description, 118 - parsed.tags, 119 - parsed.project 120 - ]); 121 - setNewText(""); 122 - } 123 - } 90 + const handleActionChange = (action: string) => { 91 + setCurrentAction(action); 92 + }; 124 93 125 - const parseTodos = (e) => { 126 - // On enter execute the command 127 - if (e.key === "Enter") { 128 - console.log("enter confirmed") 129 - try { 130 - const parsed = commandParser.parse(e.target.value); 131 - // TODO: 132 - // We need to identify the type of the command first 133 - // Currently assumes that it is done 134 - switch (parsed.type) { 135 - case "done": 136 - if (parsed.filters && parsed.filters.length > 0) { 137 - const idFilters = parsed.filters.filter(f => f.type === "id"); 138 - 139 - // Get the row IDs from the table 140 - const rowIds = idFilters.flatMap(filter => 141 - filter.ids.map(id => todos[id - 1]?.id) 142 - ).filter(Boolean); 94 + const executeCommand = (value: string) => { 95 + try { 96 + console.log(value) 97 + const parsed = commandParser.parse(value); 98 + // TODO: 99 + // We need to identify the type of the command first 100 + // Currently assumes that it is done 101 + switch (parsed.type) { 102 + case "done": 103 + if (parsed.filters && parsed.filters.length > 0) { 104 + const idFilters = parsed.filters.filter(f => f.type === "id"); 143 105 144 - // Execute SQL update for each row 145 - if (rowIds.length > 0) { 146 - rowIds.forEach(id => { 147 - ctx.db.exec( 148 - `UPDATE todos SET completed = 1 WHERE id = ?`, 149 - [id] 150 - ); 151 - }); 152 - } 106 + // Get the row IDs from the table 107 + const rowIds = idFilters.flatMap(filter => 108 + filter.ids.map(id => todos[id - 1]?.id) 109 + ).filter(Boolean); 153 110 154 - setNewText(""); 155 - setRowSelection({}); 111 + // Execute SQL update for each row 112 + if (rowIds.length > 0) { 113 + rowIds.forEach(id => { 114 + ctx.db.exec( 115 + `UPDATE todos SET completed = 1 WHERE id = ?`, 116 + [id] 117 + ); 118 + }); 156 119 } 120 + break; 121 + } 157 122 case "add": 158 123 ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed) 159 124 VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`, ··· 162 127 parsed.tags, 163 128 parsed.project 164 129 ]); 165 - setNewText(""); 166 - } 167 - } catch (error) { 168 - // TODO: 169 - // This is actually bad 170 - // We want to throw an error to the user here 171 - console.log("Unable to parse field"); 172 - return; 130 + break; 173 131 } 132 + console.log("Why won't you clear?!") 133 + setNewText(""); 134 + setRowSelection({}); 135 + } catch (error) { 136 + // TODO: 137 + // This is actually bad 138 + // We want to throw an error to the user here 139 + console.log("Unable to parse field onExecute"); 140 + console.log(error); 141 + return; 142 + } 143 + } 144 + 145 + const parseTodos = (e) => { 146 + // On enter execute the command 147 + if (e.key === "Enter") { 148 + console.log("enter confirmed") 149 + executeCommand(currentAction + " " + e.target.value); 174 150 } 175 151 // React to key presses for selection 176 152 // TODO: ··· 179 155 // Rather than changes to the input value 180 156 else if (e.target.value.trim() !== "") { 181 157 try { 182 - const parsed = commandParser.parse(e.target.value); 158 + const parsed = commandParser.parse(currentAction + " " + e.target.value); 183 159 // TODO: 184 160 // We want to reactively update based on command still 185 161 // Right now this assumes the command is filter 186 - 162 + 187 163 if (parsed.filters && parsed.filters.length > 0) { 188 164 const idFilters = parsed.filters.filter(f => f.type === "id"); 189 - 165 + 190 166 // TODO: 191 167 // We should probably have selection state react to changes on NewText 192 168 // Rather than manually updating here 193 169 194 170 // Create new selection state 195 171 const newSelection = {}; 196 - idFilters.forEach(filter => { 197 - filter.ids.forEach(id => { 198 - newSelection[id-1] = true; 199 - }) 200 - }); 201 - 172 + idFilters.forEach(filter => { 173 + filter.ids.forEach(id => { 174 + newSelection[id - 1] = true; 175 + }) 176 + }); 177 + 202 178 setRowSelection(newSelection); 203 179 } 204 180 } catch (error) { 205 - console.log("Unable to parse field") 181 + console.log("Unable to parse field onUpdate") 206 182 return; 207 183 } 208 184 } else { ··· 213 189 return ( 214 190 <> 215 191 <section className="pt-8 container"> 216 - <Input type="text" 217 - className="bg-background" 218 - placeholder="..." 219 - value={newText} 220 - onKeyUp={parseTodos} 221 - onChange={(e) => setNewText(e.target.value)} 222 - /> 223 - <div className="p-2"/> 224 - <DataTable 225 - columns={columns} 226 - data={todos} 227 - rowSelection={rowSelection} 228 - setRowSelection={setRowSelection} 229 - setNewText={setNewText} 230 - newText={newText} 231 - /> 192 + <Input type="text" 193 + className="bg-background" 194 + placeholder="..." 195 + value={newText} 196 + onKeyUp={parseTodos} 197 + onActionChange={handleActionChange} 198 + onChange={(e) => setNewText(e.target.value)} 199 + onSubmit={executeCommand} 200 + /> 201 + <div className="p-2" /> 202 + <DataTable 203 + columns={columns} 204 + data={todos} 205 + rowSelection={rowSelection} 206 + setRowSelection={setRowSelection} 207 + setNewText={setNewText} 208 + newText={newText} 209 + /> 232 210 </section> 233 211 </> 234 212 )
+57
mast-react-vite/src/components/ui/button.tsx
··· 1 + import * as React from "react" 2 + import { Slot } from "@radix-ui/react-slot" 3 + import { cva, type VariantProps } from "class-variance-authority" 4 + 5 + import { cn } from "@/lib/utils" 6 + 7 + const buttonVariants = cva( 8 + "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", 9 + { 10 + variants: { 11 + variant: { 12 + default: 13 + "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 + destructive: 15 + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 + outline: 17 + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 + secondary: 19 + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 + ghost: "hover:bg-accent hover:text-accent-foreground", 21 + link: "text-primary underline-offset-4 hover:underline", 22 + }, 23 + size: { 24 + default: "h-9 px-4 py-2", 25 + sm: "h-8 rounded-md px-3 text-xs", 26 + lg: "h-10 rounded-md px-8", 27 + icon: "h-9 w-9", 28 + }, 29 + }, 30 + defaultVariants: { 31 + variant: "default", 32 + size: "default", 33 + }, 34 + } 35 + ) 36 + 37 + export interface ButtonProps 38 + extends React.ButtonHTMLAttributes<HTMLButtonElement>, 39 + VariantProps<typeof buttonVariants> { 40 + asChild?: boolean 41 + } 42 + 43 + const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 44 + ({ className, variant, size, asChild = false, ...props }, ref) => { 45 + const Comp = asChild ? Slot : "button" 46 + return ( 47 + <Comp 48 + className={cn(buttonVariants({ variant, size, className }))} 49 + ref={ref} 50 + {...props} 51 + /> 52 + ) 53 + } 54 + ) 55 + Button.displayName = "Button" 56 + 57 + export { Button, buttonVariants }
+41 -18
mast-react-vite/src/components/ui/input.tsx
··· 1 1 import * as React from "react" 2 2 3 3 import { cn } from "@/lib/utils" 4 + import { Button } from "@/components/ui/button" 4 5 6 + // Add onActionChange to InputProps interface 5 7 export interface InputProps 6 - extends React.InputHTMLAttributes<HTMLInputElement> {} 8 + extends React.InputHTMLAttributes<HTMLInputElement> { 9 + onActionChange?: (action: string) => void; 10 + onSubmit?: (value: string) => void; // Add this prop 11 + } 7 12 8 13 const Input = React.forwardRef<HTMLInputElement, InputProps>( 9 - ({ className, type, ...props }, ref) => { 10 - const [selectedIndex, setSelectedIndex] = React.useState(0) 11 - const options = ["add", "done", "delete"] 12 - 13 - const handleWheel = (event: React.WheelEvent) => { 14 + ({ className, type, onActionChange, onSubmit, value, ...props }, ref) => { 15 + const [selectedIndex, setSelectedIndex] = React.useState(0) 16 + const options = ["add", "filter", "done", "delete"] 17 + 18 + const handleWheel = (event: React.WheelEvent) => { 14 19 event.preventDefault() 15 20 if (event.deltaY > 0) { 16 - // Scroll down 17 - setSelectedIndex((prev) => (prev + 1) % options.length) 21 + const newIndex = (selectedIndex + 1) % options.length 22 + setSelectedIndex(newIndex) 23 + onActionChange?.(options[newIndex]) 18 24 } else { 19 - // Scroll up 20 - setSelectedIndex((prev) => (prev - 1 + options.length) % options.length) 25 + const newIndex = (selectedIndex - 1 + options.length) % options.length 26 + setSelectedIndex(newIndex) 27 + onActionChange?.(options[newIndex]) 28 + } 29 + } 30 + 31 + const handleSubmit = () => { 32 + if (onSubmit && value) { 33 + console.log("trigger button") 34 + onSubmit(options[selectedIndex] + " " + value); 21 35 } 22 36 } 23 - 37 + 24 38 return ( 25 - <div className="relative" 26 - onWheel={handleWheel} 27 - > 28 - <select 39 + <div className="relative items-center" onWheel={handleWheel}> 40 + <select 29 41 className={cn( 30 42 "absolute top-0 left-0 h-9 rounded-l-md border-r border-input bg-transparent px-2 text-sm cursor-default", 31 - "shadow-sm transition-all duration-200 text-center appearance-none", 43 + "shadow-sm transition-all duration-200 text-center appearance-none", 32 44 )} 33 45 value={options[selectedIndex]} 34 46 onChange={(e) => { 35 - setSelectedIndex(options.indexOf(e.target.value)) 47 + const newIndex = options.indexOf(e.target.value) 48 + setSelectedIndex(newIndex) 49 + onActionChange?.(e.target.value) 36 50 }} 37 51 > 38 52 <option value="" disabled selected> ··· 47 61 <input 48 62 type={type} 49 63 className={cn( 50 - "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", 64 + "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", 51 65 className 52 66 )} 53 67 ref={ref} 68 + value={value} 54 69 {...props} 55 70 /> 71 + <Button 72 + type="button" 73 + onClick={handleSubmit} 74 + className="absolute right-1 top-1/2 -translate-y-1/2 h-7 z-10" 75 + variant="secondary" 76 + > 77 + Submit 78 + </Button> 56 79 </div> 57 80 ) 58 81 }
+11
mast-react-vite/src/index.css
··· 67 67 @apply bg-background text-foreground; 68 68 } 69 69 } 70 + 71 + 72 + 73 + @layer base { 74 + * { 75 + @apply border-border outline-ring/50; 76 + } 77 + body { 78 + @apply bg-background text-foreground; 79 + } 80 + } 70 81