added ImageUpload and other input types.

This commit is contained in:
2026-04-01 15:51:25 +05:30
parent 14dcd19b17
commit 3b472242a7
6 changed files with 171 additions and 23 deletions

View File

@@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider, useAuth, AuthPage } from "../auth/src"; import { AuthProvider, useAuth, AuthPage } from "../auth/src";
import { UploadProvider } from "./providers/UploadProvider";
import AdminLayout from "./components/AdminLayout"; import AdminLayout from "./components/AdminLayout";
import ResourceView from "./components/ResourceView"; import ResourceView from "./components/ResourceView";
import { config } from "./config"; import { config } from "./config";
@@ -69,7 +70,9 @@ export default function App() {
<AppTheme> <AppTheme>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider authBaseUrl={config.authBaseUrl}> <AuthProvider authBaseUrl={config.authBaseUrl}>
<UploadProvider>
<AdminApp /> <AdminApp />
</UploadProvider>
</AuthProvider> </AuthProvider>
</QueryClientProvider> </QueryClientProvider>
</AppTheme> </AppTheme>

View File

@@ -13,6 +13,8 @@ import {
Divider, Divider,
} from '@mui/material'; } from '@mui/material';
import { ResourceConfig, ResourceField } from '../types/config'; import { ResourceConfig, ResourceField } from '../types/config';
import { useUpload } from '../providers/UploadProvider';
import ImageUploadField from './fields/ImageUploadField';
interface GenericFormProps { interface GenericFormProps {
config: ResourceConfig; config: ResourceConfig;
@@ -27,9 +29,10 @@ export default function GenericForm({
initialData = {}, initialData = {},
onSave, onSave,
onCancel, onCancel,
loading, loading: saving,
}: GenericFormProps) { }: GenericFormProps) {
const [formData, setFormData] = React.useState(initialData); const [formData, setFormData] = React.useState(initialData);
const { uploadFile, uploading } = useUpload();
const handleChange = (key: string, value: any) => { const handleChange = (key: string, value: any) => {
setFormData((prev: any) => ({ ...prev, [key]: value })); setFormData((prev: any) => ({ ...prev, [key]: value }));
@@ -53,16 +56,18 @@ export default function GenericForm({
name={key} name={key}
field={field} field={field}
value={formData[key]} value={formData[key]}
onChange={(val) => handleChange(key, val)} onChange={(val: any) => handleChange(key, val)}
disabled={field.readOnly} disabled={field.readOnly}
uploadFile={uploadFile}
uploading={uploading}
/> />
))} ))}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4 }}>
<Button variant="outlined" onClick={onCancel} disabled={loading}> <Button variant="outlined" onClick={onCancel} disabled={saving}>
Cancel Cancel
</Button> </Button>
<Button variant="contained" type="submit" loading={loading} disabled={loading}> <Button variant="contained" type="submit" loading={saving} disabled={saving || uploading}>
Save {config.label} Save {config.label}
</Button> </Button>
</Box> </Box>
@@ -70,9 +75,23 @@ export default function GenericForm({
); );
} }
function FormField({ name, field, value, onChange, disabled }: any) { function FormField({ name, field, value, onChange, disabled, uploadFile, uploading }: any) {
const label = field.label; const label = field.label;
if (field.type === 'image') {
return (
<ImageUploadField
label={label}
value={value}
onUpload={async (file) => {
const url = await uploadFile(file);
if (url) onChange(url);
}}
uploading={uploading}
/>
);
}
if (field.type === 'boolean') { if (field.type === 'boolean') {
return ( return (
<FormControlLabel <FormControlLabel
@@ -108,6 +127,36 @@ function FormField({ name, field, value, onChange, disabled }: any) {
); );
} }
if (field.type === 'datetime') {
return (
<TextField
fullWidth
label={label}
type="datetime-local"
InputLabelProps={{ shrink: true }}
value={value ? new Date(value).toISOString().slice(0, 16) : ''}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
required={field.required}
/>
);
}
if (field.type === 'date') {
return (
<TextField
fullWidth
label={label}
type="date"
InputLabelProps={{ shrink: true }}
value={value ? new Date(value).toISOString().split('T')[0] : ''}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
required={field.required}
/>
);
}
if (field.type === 'markdown' || field.type === 'string') { if (field.type === 'markdown' || field.type === 'string') {
return ( return (
<TextField <TextField
@@ -137,21 +186,6 @@ function FormField({ name, field, value, onChange, disabled }: any) {
); );
} }
if (field.type === 'date') {
return (
<TextField
fullWidth
label={label}
type="date"
InputLabelProps={{ shrink: true }}
value={value ? new Date(value).toISOString().split('T')[0] : ''}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
required={field.required}
/>
);
}
return ( return (
<TextField <TextField
fullWidth fullWidth

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import { Box, Button, Avatar, CircularProgress, Typography } from "@mui/material";
import { config } from "../../config";
interface ImageUploadFieldProps {
label?: string;
value: string;
uploading?: boolean;
onUpload: (file: File) => void;
size?: number;
}
export default function ImageUploadField({
label = "Upload Image",
value,
uploading = false,
onUpload,
size = 64,
}: ImageUploadFieldProps) {
const imgSrc = value
? config.baseUrl.replace(/\/+$/, "") +
"/" +
value.replace(/^\/+/, "")
: "";
return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1, mb: 3 }}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Avatar
src={imgSrc}
sx={{ width: size, height: size, borderRadius: 2 }}
/>
<Button
variant="outlined"
component="label"
disabled={uploading}
startIcon={uploading && <CircularProgress size={16} />}
>
{uploading ? "Uploading..." : "Choose File"}
<input
type="file"
accept="image/*"
hidden
onChange={(e) => {
const file = e.target.files?.[0];
if (file) onUpload(file);
}}
/>
</Button>
</Box>
</Box>
);
}

View File

@@ -17,6 +17,7 @@ const AccountResource: ResourceConfig = {
}, },
currency: { type: "string", label: "Currency", required: true }, currency: { type: "string", label: "Currency", required: true },
is_active: { type: "boolean", label: "Active" }, is_active: { type: "boolean", label: "Active" },
avatar: { type: "image", label: "Account Icon" },
}, },
}; };
@@ -42,7 +43,7 @@ const ExpenseResource: ResourceConfig = {
fields: { fields: {
id: { type: "string", label: "ID", readOnly: true }, id: { type: "string", label: "ID", readOnly: true },
amount: { type: "number", label: "Amount", required: true }, amount: { type: "number", label: "Amount", required: true },
occurred_at: { type: "date", label: "Occurred At", required: true }, occurred_at: { type: "datetime", label: "Occurred At", required: true },
payee: { payee: {
type: "object", type: "object",
label: "Payee", label: "Payee",
@@ -58,7 +59,7 @@ const ExpenseResource: ResourceConfig = {
}, },
account: { type: "string", label: "Account ID", required: true }, account: { type: "string", label: "Account ID", required: true },
tags: { type: "array", label: "Tags" }, tags: { type: "array", label: "Tags" },
created_at: { type: "date", label: "Created At", readOnly: true }, created_at: { type: "datetime", label: "Created At", readOnly: true },
}, },
}; };

View File

@@ -0,0 +1,52 @@
import React, { createContext, useContext, useState } from "react";
import { api } from "../api/client";
export interface UploadContextModel {
uploadFile: (file: File) => Promise<string | null>;
uploading: boolean;
error: string | null;
}
const UploadContext = createContext<UploadContextModel | undefined>(undefined);
export const UploadProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const uploadFile = async (file: File): Promise<string | null> => {
setUploading(true);
setError(null);
try {
const arrayBuffer = await file.arrayBuffer();
const binary = new Uint8Array(arrayBuffer);
const res = await api.post("/uploads", binary, {
headers: {
"Content-Type": file.type,
"Content-Disposition": `attachment; filename="${file.name}"`,
},
});
return res.data.url as string;
} catch (err: any) {
console.error("File upload failed:", err);
setError(err.response?.data?.detail || "Failed to upload file");
return null;
} finally {
setUploading(false);
}
};
return (
<UploadContext.Provider value={{ uploadFile, uploading, error }}>
{children}
</UploadContext.Provider>
);
};
export const useUpload = (): UploadContextModel => {
const ctx = useContext(UploadContext);
if (!ctx) throw new Error("useUpload must be used within UploadProvider");
return ctx;
};

View File

@@ -3,8 +3,10 @@ export type FieldType =
| 'number' | 'number'
| 'boolean' | 'boolean'
| 'date' | 'date'
| 'datetime'
| 'markdown' | 'markdown'
| 'enum' | 'enum'
| 'image'
| 'object' | 'object'
| 'array'; | 'array';