Skip to content

Commit

Permalink
add action cable updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle Zarazan committed Nov 22, 2024
1 parent c66d580 commit dce34af
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 13 deletions.
4 changes: 4 additions & 0 deletions app/channels/application_cable/channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
4 changes: 4 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end
8 changes: 8 additions & 0 deletions app/channels/recipe_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class RecipeChannel < ApplicationCable::Channel
def subscribed
stream_from "recipe_channel"
end

def unsubscribed
end
end
9 changes: 9 additions & 0 deletions app/controllers/recipes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ def update
end
end

def destroy
@recipe = Recipe.find(params[:id])
if @recipe.destroy
render :create, status: :ok
else
render :errors, status: :unprocessable_entity
end
end

private

def recipe_params
Expand Down
1 change: 0 additions & 1 deletion app/javascript/application.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from "react";
import * as ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import App from "./components/App";

ReactDOM.createRoot(document.getElementById("app")!).render(
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/channels/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createConsumer } from "@rails/actioncable"

const consumer = createConsumer()

export default consumer
21 changes: 21 additions & 0 deletions app/javascript/channels/recipe_channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

import { store } from "../store/store"
import { recipeUpdated } from "../store/recipesSlice"
import consumer from "./consumer"

consumer.subscriptions.create("RecipeChannel", {
connected() {
console.log("Connected to RecipeChannel")
},

disconnected() {
console.log("Disconnected from RecipeChannel")
},

received(data) {
if (data.type === 'RECIPE_UPDATED') {
console.log("Received recipe update:", data.recipe)
store.dispatch(recipeUpdated(data.recipe))
}
}
})
2 changes: 2 additions & 0 deletions app/javascript/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Foods from "./foods/FoodsIndex";
import NewRecipeForm from './recipes/RecipeNew';
import RecipeEdit from './recipes/RecipeEdit';

import "../channels/recipe_channel"

const App: React.FC = () => {
return (
<Provider store={store}>
Expand Down
41 changes: 34 additions & 7 deletions app/javascript/components/recipes/RecipeEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { updateRecipe } from '../../store/recipesSlice';
import { updateRecipe, deleteRecipe } from '../../store/recipesSlice';
import { RecipeFormData } from './RecipeForm';
import useCsrfToken from '../../hooks/useCsrfToken';
import { useNavigate, useParams } from 'react-router-dom';
Expand Down Expand Up @@ -51,17 +51,44 @@ const EditRecipeForm: React.FC = () => {
});
};

const handleDelete = () => {
if (window.confirm('Are you sure you want to delete this recipe? This action cannot be undone.')) {
dispatch(deleteRecipe({
id: Number(id),
csrfToken
}))
.unwrap()
.then(() => {
navigate('/recipes');
})
.catch((error: Error) => {
alert('Failed to delete recipe: ' + error.message);
});
}
};

if (!recipe) {
return <div>Recipe not found</div>;
}

return (
<RecipeForm
formData={formData}
setFormData={setFormData}
onSubmit={handleSubmit}
submitButtonText="Update Recipe"
/>
<div className="max-w-2xl mx-auto p-4">
<h2 className="text-2xl font-bold mb-6">Edit Recipe</h2>
<RecipeForm
formData={formData}
setFormData={setFormData}
onSubmit={handleSubmit}
submitButtonText="Update Recipe"
/>
<div className="mt-6 border-t pt-6">
<button
onClick={handleDelete}
className="w-full bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Delete Recipe
</button>
</div>
</div>
);
};

Expand Down
2 changes: 1 addition & 1 deletion app/javascript/components/recipes/RecipeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const RecipeForm: React.FC<RecipeFormProps> = ({ formData, setFormData, onSubmit
<div className="mb-4">
<label className="block mb-2">Description:</label>
<textarea
value={formData.description}
value={formData.description || ''}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full p-2 border rounded"
/>
Expand Down
35 changes: 33 additions & 2 deletions app/javascript/store/recipesSlice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { RecipeFormData } from '../components/recipes/RecipeNew';
import { RecipeFormData } from '../components/recipes/RecipeForm';
import { Recipe } from '../types/types';

export const fetchRecipes = createAsyncThunk(
Expand Down Expand Up @@ -52,6 +52,25 @@ export const updateRecipe = createAsyncThunk(
}
);

export const deleteRecipe = createAsyncThunk(
'recipes/delete',
async ({ id, csrfToken }: { id: number; csrfToken: string }) => {
const response = await fetch(`/api/recipes/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': csrfToken
}
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'An error occurred');
}

return response.json();
}
);

interface RecipesState {
items: Recipe[];
status: 'idle' | 'loading' | 'succeeded' | 'failed';
Expand All @@ -67,7 +86,14 @@ const initialState: RecipesState = {
const recipesSlice = createSlice({
name: 'recipes',
initialState,
reducers: {},
reducers: {
recipeUpdated: (state, action) => {
const index = state.items.findIndex(recipe => recipe.id === action.payload.id)
if (index !== -1) {
state.items[index] = action.payload
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchRecipes.pending, (state) => {
Expand All @@ -94,7 +120,12 @@ const recipesSlice = createSlice({
}
state.status = 'succeeded'
})
.addCase(deleteRecipe.fulfilled, (state, action) => {
state.items = state.items.filter(recipe => recipe.id !== action.payload.recipe.id)
state.status = 'succeeded'
})
},
})

export const { recipeUpdated } = recipesSlice.actions
export default recipesSlice.reducer
19 changes: 18 additions & 1 deletion app/models/recipe.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
class Recipe < ApplicationRecord
has_many :ingredients
has_many :ingredients, dependent: :destroy

accepts_nested_attributes_for :ingredients, allow_destroy: true

after_save :broadcast_recipe

def to_json
ApplicationController.new.view_context.render(
partial: "recipes/recipe",
locals: { recipe: self },
formats: [:json],

Check failure on line 12 in app/models/recipe.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.
handlers: [:jbuilder]
)
end

private

def broadcast_recipe
ActionCable.server.broadcast 'recipe_channel', { type: 'RECIPE_UPDATED', recipe: self.to_json }
end
end
5 changes: 5 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@

# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate!

config.assets.debug = true
config.assets.quiet = true
config.assets.compile = true
config.assets.source_maps = true
end
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
root "home#index"

scope "/api" do
resources :recipes, only: [:index, :create, :update]
resources :recipes
resources :foods, only: [:index]
end

Expand Down
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify"
},
"dependencies": {
"@hotwired/turbo-rails": "^8.0.12",
"@rails/actioncable": "^8.0.0",
"@reduxjs/toolkit": "^2.3.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
Expand All @@ -18,6 +20,7 @@
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@types/rails__actioncable": "^6.1.11",
"eslint": "^9.15.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0",
Expand Down

0 comments on commit dce34af

Please sign in to comment.