- <%= link_to "Destroy this product", product,
- data: { turbo_method: :delete, turbo_confirm: "Are you sure?" },
- class: "btn shadow-none text-rose-500 mr-3" if product.persisted? %>
- <%= link_to "Back", product.persisted? ? kit_products_path : product, class: "btn border-slate-200 hover:border-slate-300 text-slate-600" %>
- <%= form.button :submit, class: "bg-indigo-500 hover:bg-indigo-600 text-white ml-3" %>
+
+ <%= simple_form_for product, html: { class: 'grow space-y-6' } do |form| %>
+ <% if product.errors.any? %>
+
+
<%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:
+
+ <% end %>
+
+
+ <%= form.input :name %>
-
+
+ <% end %>
<% end %>
diff --git a/app/views/kit/products/_product.json.jbuilder b/app/views/kit/products/_product.json.jbuilder
index 396313e..a4cc307 100644
--- a/app/views/kit/products/_product.json.jbuilder
+++ b/app/views/kit/products/_product.json.jbuilder
@@ -1,4 +1,4 @@
# frozen_string_literal: true
-json.extract!(kit_product, :id, :name, :created_at, :updated_at)
+json.extract!(kit_product, :id, :name)
json.url(api_kit_product_url(kit_product, format: :json))
diff --git a/app/views/kit/products/create.turbo_stream.erb b/app/views/kit/products/create.turbo_stream.erb
new file mode 100644
index 0000000..5a05b42
--- /dev/null
+++ b/app/views/kit/products/create.turbo_stream.erb
@@ -0,0 +1,2 @@
+<%= turbo_stream.update "#{@kit_product.model_name.singular}_form", partial: "kit/products/product", locals: { lash: flash, product: @kit_product } %>
+<%= turbo_stream.append "list_#{@kit_product.model_name.plural}", inline: render(SearchListItemComponent.new(@kit_product)) if @kit_product.persisted? %>
diff --git a/app/views/kit/products/destroy.turbo_stream.erb b/app/views/kit/products/destroy.turbo_stream.erb
new file mode 100644
index 0000000..2152604
--- /dev/null
+++ b/app/views/kit/products/destroy.turbo_stream.erb
@@ -0,0 +1,3 @@
+<%= turbo_stream.remove dom_id(@kit_product, 'list') %>
+<%= turbo_stream.update "#{@kit_product.model_name.singular}_form", partial: "kit/products/product", locals: { flash: flash, product: Kit::Product.new } %>
+
diff --git a/app/views/kit/products/index.html+turbo_frame.erb b/app/views/kit/products/index.html+turbo_frame.erb
new file mode 100644
index 0000000..d81f0fb
--- /dev/null
+++ b/app/views/kit/products/index.html+turbo_frame.erb
@@ -0,0 +1,3 @@
+<%= render SearchListComponent.new(resource: @q.object.model_name) do |c| %>
+ <%= @kit_products.each { |kit_product| c.with_item(kit_product) } %>
+<% end %>
diff --git a/app/views/kit/products/index.html.erb b/app/views/kit/products/index.html.erb
index 53a3738..c82f5e9 100644
--- a/app/views/kit/products/index.html.erb
+++ b/app/views/kit/products/index.html.erb
@@ -17,7 +17,7 @@
-
+
<%= render @kit_product %>
diff --git a/app/views/kit/products/show.html+turbo_frame.erb b/app/views/kit/products/show.html+turbo_frame.erb
new file mode 100644
index 0000000..5bc20b5
--- /dev/null
+++ b/app/views/kit/products/show.html+turbo_frame.erb
@@ -0,0 +1 @@
+<%= render @kit_product %>
diff --git a/app/views/kit/products/update.turbo_stream.erb b/app/views/kit/products/update.turbo_stream.erb
new file mode 100644
index 0000000..a14128d
--- /dev/null
+++ b/app/views/kit/products/update.turbo_stream.erb
@@ -0,0 +1,2 @@
+<%= turbo_stream.update dom_id(@kit_product, 'list'), inline: render(SearchListItemComponent.new(@kit_product)) %>
+<%= turbo_stream.update "#{@kit_product.model_name.singular}_form", partial: "kit/products/product", locals: { flash: flash, product: @kit_product } %>
diff --git a/app/views/layouts/update.turbo_stream.erb b/app/views/layouts/update.turbo_stream.erb
new file mode 100644
index 0000000..a14128d
--- /dev/null
+++ b/app/views/layouts/update.turbo_stream.erb
@@ -0,0 +1,2 @@
+<%= turbo_stream.update dom_id(@kit_product, 'list'), inline: render(SearchListItemComponent.new(@kit_product)) %>
+<%= turbo_stream.update "#{@kit_product.model_name.singular}_form", partial: "kit/products/product", locals: { flash: flash, product: @kit_product } %>
diff --git a/bin/render-build.sh b/bin/render-build.sh
new file mode 100755
index 0000000..00700a0
--- /dev/null
+++ b/bin/render-build.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# exit on error
+set -o errexit
+
+bundle install
+bundle exec rake assets:precompile
+bundle exec rake assets:clean
+bundle exec rake db:migrate
diff --git a/config/database.yml b/config/database.yml
index fcba57f..83a6543 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -22,4 +22,5 @@ test:
production:
<<: *default
- database: db/production.sqlite3
+ adapter: postgresql
+ url: <%= ENV['DATABASE_URL'] %>
diff --git a/config/environments/production.rb b/config/environments/production.rb
index a154870..66f4c04 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -22,7 +22,7 @@
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
- config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? || ENV['RENDER'].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb
new file mode 100644
index 0000000..9937e1e
--- /dev/null
+++ b/config/initializers/cors.rb
@@ -0,0 +1,6 @@
+Rails.application.config.middleware.insert_before 0, Rack::Cors do
+ allow do
+ origins '*'
+ resource '*', headers: :any, methods: [:get, :post, :patch, :put, :delete]
+ end
+end
diff --git a/config/puma.rb b/config/puma.rb
index daaf036..81dae67 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -30,14 +30,14 @@
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
-# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+workers ENV.fetch("WEB_CONCURRENCY") { 4 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
-# preload_app!
+preload_app!
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
diff --git a/db/migrate/20221022131756_create_kit_components.rb b/db/migrate/20221022131756_create_kit_components.rb
index 32a5cb1..795ca19 100644
--- a/db/migrate/20221022131756_create_kit_components.rb
+++ b/db/migrate/20221022131756_create_kit_components.rb
@@ -3,7 +3,7 @@
class CreateKitComponents < ActiveRecord::Migration[7.0]
def change
create_table(:kit_components) do |t|
- t.references(:product, null: false, foreign_key: true)
+ t.references(:product, null: false, foreign_key: { to_table: :kit_products })
t.string(:name)
t.timestamps
diff --git a/db/schema.rb b/db/schema.rb
index 59cddb3..7ea3aba 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1,5 +1,3 @@
-# frozen_string_literal: true
-
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@@ -12,20 +10,20 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 20_221_022_131_756) do
- create_table 'kit_components', force: :cascade do |t|
- t.integer('product_id', null: false)
- t.string('name')
- t.datetime('created_at', null: false)
- t.datetime('updated_at', null: false)
- t.index(['product_id'], name: 'index_kit_components_on_product_id')
+ActiveRecord::Schema[7.0].define(version: 2022_10_22_131756) do
+ create_table "kit_components", force: :cascade do |t|
+ t.integer "product_id", null: false
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["product_id"], name: "index_kit_components_on_product_id"
end
- create_table 'kit_products', force: :cascade do |t|
- t.string('name')
- t.datetime('created_at', null: false)
- t.datetime('updated_at', null: false)
+ create_table "kit_products", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
- add_foreign_key 'kit_components', 'kit_products', column: 'product_id'
+ add_foreign_key "kit_components", "kit_products", column: "product_id"
end
diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/frontend/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..4f360c8
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,38 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+.vscode
diff --git a/frontend/.nvmrc b/frontend/.nvmrc
new file mode 100644
index 0000000..9dfef47
--- /dev/null
+++ b/frontend/.nvmrc
@@ -0,0 +1 @@
+v18.12.0
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..c87e042
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,34 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
+
+[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
+
+The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/frontend/next.config.js b/frontend/next.config.js
new file mode 100644
index 0000000..156c8aa
--- /dev/null
+++ b/frontend/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+ swcMinify: true,
+ pageExtensions: ["jsx", "tsx"],
+};
+
+module.exports = nextConfig;
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..899e622
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "frontend",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^2.9.10",
+ "@radix-ui/react-slot": "^1.0.1",
+ "@tanstack/react-query": "^4.16.1",
+ "axios": "^1.1.3",
+ "clsx": "^1.2.1",
+ "lodash": "^4.17.21",
+ "next": "13.0.0",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-hook-form": "^7.39.0",
+ "swr": "^1.3.0",
+ "usehooks-ts": "^2.9.1",
+ "yup": "^0.32.11"
+ },
+ "devDependencies": {
+ "@tanstack/react-query-devtools": "^4.16.1",
+ "@types/lodash": "^4.14.186",
+ "@types/node": "18.11.7",
+ "@types/react": "18.0.24",
+ "@types/react-dom": "18.0.8",
+ "autoprefixer": "^10.4.13",
+ "eslint": "8.26.0",
+ "eslint-config-next": "13.0.0",
+ "postcss": "^8.4.18",
+ "sass": "^1.55.0",
+ "tailwindcss": "^3.2.1",
+ "typescript": "4.8.4"
+ }
+}
diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx
new file mode 100644
index 0000000..1c1a6e8
--- /dev/null
+++ b/frontend/pages/_app.tsx
@@ -0,0 +1,35 @@
+import type { AppProps } from "next/app";
+import {
+ Hydrate,
+ QueryClient,
+ QueryClientProvider,
+} from "@tanstack/react-query";
+
+import Layout from "@/components/Layout";
+
+import "../styles/globals.css";
+import { useState } from "react";
+
+export default function App({ Component, pageProps }: AppProps) {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ },
+ },
+ })
+ );
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx
new file mode 100644
index 0000000..0fc7c72
--- /dev/null
+++ b/frontend/pages/_document.tsx
@@ -0,0 +1,13 @@
+import { Html, Head, Main, NextScript } from "next/document";
+
+export default function Document() {
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx
new file mode 100644
index 0000000..9351b5c
--- /dev/null
+++ b/frontend/pages/index.tsx
@@ -0,0 +1,5 @@
+import Link from "next/link";
+
+export default function Home() {
+ return
Products;
+}
diff --git a/frontend/pages/kit/products/[id].tsx b/frontend/pages/kit/products/[id].tsx
new file mode 100644
index 0000000..5ca55e9
--- /dev/null
+++ b/frontend/pages/kit/products/[id].tsx
@@ -0,0 +1,50 @@
+import { GetServerSideProps } from "next/types";
+import { dehydrate, QueryClient } from "@tanstack/react-query";
+
+import { findAll, findById, Product } from "./services";
+
+import Form from "@/src/components/Products/Form";
+import Sidebar from "@/components/Products/Sidebar";
+import * as Page from "@/components/Layout/Page";
+import { ProductsProvider } from "@/contexts/products";
+
+interface ProductsPageProps {
+ product: Product;
+ fallback: Record