Last active
September 8, 2025 13:02
-
-
Save arman-h/7af4face5429e920e71ebcdd6ae17526 to your computer and use it in GitHub Desktop.
Rails + Interia
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // components/ResourceableControls.jsx | |
| import useResourceableStore from '@/stores/resourceableStore' | |
| /** | |
| * This renders the Edit/Save/Cancel buttons. | |
| * It's generic and works with any resource type. | |
| */ | |
| export default function ResourceableControls({ | |
| resource, // The resource being edited | |
| resourceType, // Type of resource ('user', 'case', etc) | |
| additionalControls = [] // Extra buttons (optional) | |
| }) { | |
| // Connect to our global store | |
| const { enableEdit, disableEdit, startSaving, getResourceState } = useResourceableStore() | |
| // Get current state for this resource | |
| const resourceState = getResourceState(resourceType, resource.id) | |
| /** | |
| * When user clicks the Edit button | |
| */ | |
| const handleEditClick = () => { | |
| enableEdit(resourceType, resource.id) | |
| } | |
| /** | |
| * When user clicks the Cancel button | |
| */ | |
| const handleCancelClick = () => { | |
| disableEdit(resourceType, resource.id) | |
| } | |
| /** | |
| * When user clicks the Save button | |
| * This tells the store that a save is starting, | |
| * the form will notice and submit via Inertia. | |
| */ | |
| const handleSaveClick = () => { | |
| startSaving(resourceType, resource.id) | |
| } | |
| return ( | |
| <div className="resourceable-controls"> | |
| <div className="control-buttons flex gap-3"> | |
| {/* Show Edit button when NOT editing */} | |
| {!resourceState.isEditing && ( | |
| <button | |
| onClick={handleEditClick} | |
| className="btn btn-primary" | |
| disabled={resourceState.isLoading} | |
| > | |
| Edit | |
| </button> | |
| )} | |
| {/* Show Save and Cancel buttons when editing */} | |
| {resourceState.isEditing && ( | |
| <> | |
| <button | |
| onClick={handleSaveClick} | |
| disabled={resourceState.isLoading} | |
| className="btn btn-success" | |
| > | |
| {resourceState.isLoading ? 'Saving...' : 'Save'} | |
| </button> | |
| <button | |
| onClick={handleCancelClick} | |
| disabled={resourceState.isLoading} | |
| className="btn btn-secondary" | |
| > | |
| Cancel | |
| </button> | |
| </> | |
| )} | |
| {/* Render any additional buttons passed in */} | |
| {additionalControls.map((AdditionalControl, index) => ( | |
| <AdditionalControl | |
| key={index} | |
| resource={resource} | |
| resourceType={resourceType} | |
| isEditing={resourceState.isEditing} | |
| isLoading={resourceState.isLoading} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // components/ResourceableShow.jsx | |
| import ResourceableControls from "./ResourceableControls" | |
| /** | |
| * This is a GENERIC wrapper that can be used for ANY resource type. | |
| * It renders the Edit/Save/Cancel buttons and connects them to the store. | |
| * | |
| * Usage: | |
| * <ResourceableShow resource={user} resourceType="user"> | |
| * <UserForm user={user} roles={roles} /> | |
| * </ResourceableShow> | |
| */ | |
| export default function ResourceableShow({ | |
| resource, // The resource object (user, case, project, etc) | |
| resourceType, // String identifying the type ('user', 'case', etc) | |
| children, // The form component to render inside | |
| additionalControls = [] // Extra buttons to show (optional) | |
| }) { | |
| return ( | |
| <div className="resourceable-container"> | |
| {/* The Edit/Save/Cancel buttons */} | |
| <ResourceableControls | |
| resource={resource} | |
| resourceType={resourceType} | |
| additionalControls={additionalControls} | |
| /> | |
| {/* The actual form (UserForm, CaseForm, etc) goes here */} | |
| <div className="resourceable-content"> | |
| {children} | |
| </div> | |
| </div> | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // components/UserForm.jsx | |
| import { useEffect } from 'react' | |
| import { useForm } from '@inertiajs/react' | |
| import useResourceableStore from '@/stores/resourceableStore' | |
| /** | |
| * UserForm is responsible for rendering and submitting | |
| * the form to update a user. | |
| * | |
| * This form reacts to store changes (edit/save state). | |
| */ | |
| export default function UserForm({ user, roles }) { | |
| const { data, setData, put, errors } = useForm({ | |
| name: user.name || '', | |
| email: user.email || '', | |
| role: user.role || '', | |
| }) | |
| const { getResourceState, finishSaving, disableEdit } = useResourceableStore() | |
| const resourceState = getResourceState('user', user.id) | |
| /** | |
| * Watch for saveTriggered from the store. | |
| * When Save is clicked, we submit the form via Inertia. | |
| */ | |
| useEffect(() => { | |
| if (resourceState.saveTriggered) { | |
| put(route('users.update', user.id), { | |
| preserveScroll: true, | |
| onSuccess: () => { | |
| // Let the store know saving is complete | |
| finishSaving('user', user.id) | |
| }, | |
| onError: () => { | |
| // Stop loading but stay in edit mode to fix errors | |
| finishSaving('user', user.id) | |
| enableEdit('user', user.id) | |
| } | |
| }) | |
| } | |
| }, [resourceState.saveTriggered]) | |
| /** | |
| * Handle field changes | |
| */ | |
| const handleChange = (e) => { | |
| setData(e.target.name, e.target.value) | |
| } | |
| return ( | |
| <form className="user-form space-y-4"> | |
| {/* Name field */} | |
| <div> | |
| <label htmlFor="name">Name</label> | |
| <input | |
| id="name" | |
| name="name" | |
| value={data.name} | |
| onChange={handleChange} | |
| disabled={!resourceState.isEditing || resourceState.isLoading} | |
| className="input" | |
| /> | |
| {errors.name && <div className="text-red-500">{errors.name}</div>} | |
| </div> | |
| {/* Email field */} | |
| <div> | |
| <label htmlFor="email">Email</label> | |
| <input | |
| id="email" | |
| name="email" | |
| value={data.email} | |
| onChange={handleChange} | |
| disabled={!resourceState.isEditing || resourceState.isLoading} | |
| className="input" | |
| /> | |
| {errors.email && <div className="text-red-500">{errors.email}</div>} | |
| </div> | |
| {/* Role field */} | |
| <div> | |
| <label htmlFor="role">Role</label> | |
| <select | |
| id="role" | |
| name="role" | |
| value={data.role} | |
| onChange={handleChange} | |
| disabled={!resourceState.isEditing || resourceState.isLoading} | |
| className="select" | |
| > | |
| {roles.map((role) => ( | |
| <option key={role} value={role}> | |
| {role} | |
| </option> | |
| ))} | |
| </select> | |
| {errors.role && <div className="text-red-500">{errors.role}</div>} | |
| </div> | |
| </form> | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // stores/resourceableStore.js | |
| import { create } from 'zustand' | |
| /** | |
| * This store manages the UI state for ANY type of form (user, case, project, etc). | |
| * It tracks whether a form is in "edit mode" or "view mode" | |
| * and handles loading states during save operations. | |
| * | |
| * Each resource is identified by a unique key: "resourceType-resourceId" | |
| * Example: "user-123", "case-456", "project-789" | |
| */ | |
| const useResourceableStore = create((set, get) => { | |
| /** | |
| * Helper function to generate unique key for a resource | |
| * @param {string} resourceType - The type of resource (e.g., 'user', 'case', 'project') | |
| * @param {number|string} resourceId - The ID of the specific resource | |
| * @returns {string} Unique key like "user-123" | |
| */ | |
| const getKey = (resourceType, resourceId) => `${resourceType}-${resourceId}` | |
| return { | |
| // This object stores the state for each resource by their unique key | |
| // Example: { "user-123": { isEditing: true }, "case-456": { isEditing: false } } | |
| resourceStates: {}, | |
| /** | |
| * Put a resource form into "edit mode" - makes form fields editable | |
| */ | |
| enableEdit: (resourceType, resourceId) => { | |
| const key = getKey(resourceType, resourceId) | |
| set((state) => ({ | |
| resourceStates: { | |
| ...state.resourceStates, | |
| [key]: { | |
| ...state.resourceStates[key], | |
| isEditing: true, | |
| isLoading: false, | |
| }, | |
| }, | |
| })) | |
| }, | |
| /** | |
| * Put a resource form into "view mode" - makes form fields read-only | |
| */ | |
| disableEdit: (resourceType, resourceId) => { | |
| const key = getKey(resourceType, resourceId) | |
| set((state) => ({ | |
| resourceStates: { | |
| ...state.resourceStates, | |
| [key]: { | |
| ...state.resourceStates[key], | |
| isEditing: false, | |
| isLoading: false, | |
| }, | |
| }, | |
| })) | |
| }, | |
| /** | |
| * Show loading spinner during save operations | |
| */ | |
| setLoading: (resourceType, resourceId, isLoading) => { | |
| const key = getKey(resourceType, resourceId) | |
| set((state) => ({ | |
| resourceStates: { | |
| ...state.resourceStates, | |
| [key]: { | |
| ...state.resourceStates[key], | |
| isLoading, | |
| }, | |
| }, | |
| })) | |
| }, | |
| /** | |
| * Tell the form "hey, the user clicked Save, you should submit now!" | |
| */ | |
| triggerSave: (resourceType, resourceId) => { | |
| const key = getKey(resourceType, resourceId) | |
| set((state) => ({ | |
| resourceStates: { | |
| ...state.resourceStates, | |
| [key]: { | |
| ...state.resourceStates[key], | |
| shouldSave: true, | |
| }, | |
| }, | |
| })) | |
| }, | |
| /** | |
| * Clear the "should save" flag after the form has processed it | |
| */ | |
| clearSaveTrigger: (resourceType, resourceId) => { | |
| const key = getKey(resourceType, resourceId) | |
| set((state) => ({ | |
| resourceStates: { | |
| ...state.resourceStates, | |
| [key]: { | |
| ...state.resourceStates[key], | |
| shouldSave: false, | |
| }, | |
| }, | |
| })) | |
| }, | |
| /** | |
| * Get the current state for a specific resource | |
| * Returns default values if no state exists yet | |
| */ | |
| getResourceState: (resourceType, resourceId) => { | |
| const key = getKey(resourceType, resourceId) | |
| return ( | |
| get().resourceStates[key] || { | |
| isEditing: false, | |
| isLoading: false, | |
| shouldSave: false, | |
| } | |
| ) | |
| }, | |
| /** | |
| * Clean up state when component is destroyed | |
| * Prevents memory leaks by removing unused state | |
| */ | |
| clearResourceState: (resourceType, resourceId) => { | |
| const key = getKey(resourceType, resourceId) | |
| set((state) => { | |
| const { [key]: removedState, ...remainingStates } = state.resourceStates | |
| return { resourceStates: remainingStates } | |
| }) | |
| }, | |
| } | |
| }) | |
| export default useResourceableStore |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment