From 9d2ba88989e3869fb102d476c2fdee25276c59b4 Mon Sep 17 00:00:00 2001 From: KinWang-2013 <94952098+KinWang-2013@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:09:57 +0600 Subject: [PATCH 1/3] add routes --- config/routes.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 1975468..99a0586 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do root "tasks#index" + post "tasks/:id/toggle", to: "tasks#toggle" + resources :tasks end \ No newline at end of file From 0b814386fc4772f658dd09dba45a863a54d42e03 Mon Sep 17 00:00:00 2001 From: KinWang-2013 <94952098+KinWang-2013@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:10:47 +0600 Subject: [PATCH 2/3] add hotwire --- .gitignore | 1 + Procfile.dev | 2 +- .../stylesheets/application.tailwind.css | 7 + app/controllers/tasks_controller.rb | 41 ++++ app/javascript/application.js | 3 + app/javascript/controllers/application.js | 9 + .../controllers/celebration_controller.js | 222 ++++++++++++++++++ app/javascript/controllers/index.js | 12 + .../controllers/tasks_controller.js | 28 +++ app/views/layouts/application.html.erb | 4 +- app/views/shared/_celebration.html.erb | 2 + app/views/tasks/_task.html.erb | 25 +- app/views/tasks/edit.html.erb | 8 + app/views/tasks/index.html.erb | 10 +- config/routes.rb | 2 + 15 files changed, 370 insertions(+), 6 deletions(-) create mode 100644 app/javascript/application.js create mode 100644 app/javascript/controllers/application.js create mode 100644 app/javascript/controllers/celebration_controller.js create mode 100644 app/javascript/controllers/index.js create mode 100644 app/javascript/controllers/tasks_controller.js create mode 100644 app/views/shared/_celebration.html.erb create mode 100644 app/views/tasks/edit.html.erb diff --git a/.gitignore b/.gitignore index a34dab1..1b3d867 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ /tmp/* !/log/.keep !/tmp/.keep +/.idea/* # Ignore pidfiles, but keep the directory. /tmp/pids/* diff --git a/Procfile.dev b/Procfile.dev index da151fe..0ee3bae 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,2 @@ -web: bin/rails server +web: bin/rails server --binding=192.168.22.112 -p 3000 css: bin/rails tailwindcss:watch diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 163b5b6..58ae07f 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -6,4 +6,11 @@ .btn { @apply mt-3 rounded-lg py-1 px-5 inline-block font-medium cursor-pointer; } +} + +turbo-frame { + border: 1px solid lightblue; + border-radius: 5px; + padding: 0.1em 1em; + margin: 1em 0; } \ No newline at end of file diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 9f131b8..ab5e578 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -16,6 +16,47 @@ def create end end + def edit + @task = Task.find(params[:id]) + end + + def update + @task = Task.find(params[:id]) + respond_to do |format| + if @task.update(task_params) + format.html { redirect_to tasks_url, notice: "Task was successfully updated" } + else + format.html { render :edit, status: :unprocessable_entity } + end + end + end + + def destroy + @task = Task.find(params[:id]) + @task.destroy + redirect_to tasks_url, notice: "Post was successfully deleted." + end + + def toggle + @task = Task.find(params[:id]) + @task.update(completed: params[:completed]) + + render json: { message: "Success" } + end + + def celebrate + Turbo::StreamsChannel.broadcast_replace_to( + "kinley_celeb", + target: 'kinley_celeb_frame', + html: render( + partial: 'shared/celebration', + locals: { path: '/' }, + layout: false + ) + ) + head :no_content + end + private def task_params diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..059eab2 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" \ No newline at end of file diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..57f5e91 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } \ No newline at end of file diff --git a/app/javascript/controllers/celebration_controller.js b/app/javascript/controllers/celebration_controller.js new file mode 100644 index 0000000..29b35f5 --- /dev/null +++ b/app/javascript/controllers/celebration_controller.js @@ -0,0 +1,222 @@ +import {Controller} from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + var random = Math.random + , cos = Math.cos + , sin = Math.sin + , PI = Math.PI + , PI2 = PI * 2 + , timer = undefined + , frame = undefined + , confetti = []; + + var particles = 10 + , spread = 40 + , sizeMin = 3 + , sizeMax = 12 - sizeMin + , eccentricity = 10 + , deviation = 100 + , dxThetaMin = -.1 + , dxThetaMax = -dxThetaMin - dxThetaMin + , dyMin = .13 + , dyMax = .18 + , dThetaMin = .4 + , dThetaMax = .7 - dThetaMin; + + var colorThemes = [ + function () { + return color(200 * random() | 0, 200 * random() | 0, 200 * random() | 0); + }, function () { + var black = 200 * random() | 0; + return color(200, black, black); + }, function () { + var black = 200 * random() | 0; + return color(black, 200, black); + }, function () { + var black = 200 * random() | 0; + return color(black, black, 200); + }, function () { + return color(200, 100, 200 * random() | 0); + }, function () { + return color(200 * random() | 0, 200, 200); + }, function () { + var black = 256 * random() | 0; + return color(black, black, black); + }, function () { + return colorThemes[random() < .5 ? 1 : 2](); + }, function () { + return colorThemes[random() < .5 ? 3 : 5](); + }, function () { + return colorThemes[random() < .5 ? 2 : 4](); + } + ]; + + function color(r, g, b) { + return 'rgb(' + r + ',' + g + ',' + b + ')'; + } + + // Cosine interpolation + function interpolation(a, b, t) { + return (1 - cos(PI * t)) / 2 * (b - a) + a; + } + + // Create a 1D Maximal Poisson Disc over [0, 1] + var radius = 1 / eccentricity, radius2 = radius + radius; + + function createPoisson() { + // domain is the set of points which are still available to pick from + // D = union{ [d_i, d_i+1] | i is even } + var domain = [radius, 1 - radius], measure = 1 - radius2, spline = [0, 1]; + while (measure) { + var dart = measure * random(), i, l, interval, a, b, c, d; + + // Find where dart lies + for (i = 0, l = domain.length, measure = 0; i < l; i += 2) { + a = domain[i], b = domain[i + 1], interval = b - a; + if (dart < measure + interval) { + spline.push(dart += a - measure); + break; + } + measure += interval; + } + c = dart - radius, d = dart + radius; + + // Update the domain + for (i = domain.length - 1; i > 0; i -= 2) { + l = i - 1, a = domain[l], b = domain[i]; + // c---d c---d Do nothing + // c-----d c-----d Move interior + // c--------------d Delete interval + // c--d Split interval + // a------b + if (a >= c && a < d) + if (b > d) domain[l] = d; // Move interior (Left case) + else domain.splice(l, 2); // Delete interval + else if (a < c && b > c) + if (b <= d) domain[i] = c; // Move interior (Right case) + else domain.splice(i, 0, c, d); // Split interval + } + + // Re-measure the domain + for (i = 0, l = domain.length, measure = 0; i < l; i += 2) + measure += domain[i + 1] - domain[i]; + } + + return spline.sort(); + } + + // Create the overarching container + var container = document.createElement('div'); + container.style.position = 'fixed'; + container.style.top = '0'; + container.style.left = '0'; + container.style.width = '100%'; + container.style.height = '0'; + container.style.overflow = 'visible'; + container.style.zIndex = '9999'; + + // Confetto constructor + function Confetto(theme) { + this.frame = 0; + this.outer = document.createElement('div'); + this.inner = document.createElement('div'); + this.outer.appendChild(this.inner); + + var outerStyle = this.outer.style, innerStyle = this.inner.style; + outerStyle.position = 'absolute'; + outerStyle.width = (sizeMin + sizeMax * random()) + 'px'; + outerStyle.height = (sizeMin + sizeMax * random()) + 'px'; + innerStyle.width = '100%'; + innerStyle.height = '100%'; + innerStyle.backgroundColor = theme(); + + outerStyle.perspective = '50px'; + outerStyle.transform = 'rotate(' + (360 * random()) + 'deg)'; + this.axis = 'rotate3D(' + + cos(360 * random()) + ',' + + cos(360 * random()) + ',0,'; + this.theta = 360 * random(); + this.dTheta = dThetaMin + dThetaMax * random(); + innerStyle.transform = this.axis + this.theta + 'deg)'; + + this.x = window.innerWidth * random(); + this.y = -deviation; + this.dx = sin(dxThetaMin + dxThetaMax * random()); + this.dy = dyMin + dyMax * random(); + outerStyle.left = this.x + 'px'; + outerStyle.top = this.y + 'px'; + + // Create the periodic spline + this.splineX = createPoisson(); + this.splineY = []; + for (var i = 1, l = this.splineX.length - 1; i < l; ++i) + this.splineY[i] = deviation * random(); + this.splineY[0] = this.splineY[l] = deviation * random(); + + this.update = function (height, delta) { + this.frame += delta; + this.x += this.dx * delta; + this.y += this.dy * delta; + this.theta += this.dTheta * delta; + + // Compute spline and convert to polar + var phi = this.frame % 7777 / 7777, i = 0, j = 1; + while (phi >= this.splineX[j]) i = j++; + var rho = interpolation( + this.splineY[i], + this.splineY[j], + (phi - this.splineX[i]) / (this.splineX[j] - this.splineX[i]) + ); + phi *= PI2; + + outerStyle.left = this.x + rho * cos(phi) + 'px'; + outerStyle.top = this.y + rho * sin(phi) + 'px'; + innerStyle.transform = this.axis + this.theta + 'deg)'; + return this.y > height + deviation; + }; + } + + function poof() { + if (!frame) { + // Append the container + document.body.appendChild(container); + + // Add confetti + var theme = colorThemes[0] + , count = 0; + (function addConfetto() { + var confetto = new Confetto(theme); + confetti.push(confetto); + container.appendChild(confetto.outer); + timer = setTimeout(addConfetto, spread * random()); + })(0); + + // Start the loop + var prev = undefined; + requestAnimationFrame(function loop(timestamp) { + var delta = prev ? timestamp - prev : 0; + prev = timestamp; + var height = window.innerHeight; + + for (var i = confetti.length - 1; i >= 0; --i) { + if (confetti[i].update(height, delta)) { + container.removeChild(confetti[i].outer); + confetti.splice(i, 1); + } + } + + if (timer || confetti.length) + return frame = requestAnimationFrame(loop); + + // Cleanup + document.body.removeChild(container); + frame = undefined; + }); + } + } + + poof(); + }; +} + diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..13a439d --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,12 @@ +// Import and register all your controllers from the importmap under controllers/* + +import {application} from "controllers/application" + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import {eagerLoadControllersFrom} from "@hotwired/stimulus-loading" + +eagerLoadControllersFrom("controllers", application) + +// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) +// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" +// lazyLoadControllersFrom("controllers", application) \ No newline at end of file diff --git a/app/javascript/controllers/tasks_controller.js b/app/javascript/controllers/tasks_controller.js new file mode 100644 index 0000000..6366816 --- /dev/null +++ b/app/javascript/controllers/tasks_controller.js @@ -0,0 +1,28 @@ +import {Controller} from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + console.log(this.element) + } + + toggle(e) { + const id = e.target.dataset.id + const csrfToken = document.querySelector("[name='csrf-token']").content + + fetch(`/tasks/${id}/toggle`, { + method: 'POST', // *GET, POST, PUT, DELETE, etc. + mode: 'cors', // no-cors, *cors, same-origin + cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached + credentials: 'same-origin', // include, *same-origin, omit + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, + body: JSON.stringify({completed: e.target.checked}) // body data type must match "Content-Type" header + }) + .then(response => response.json()) + .then(data => { + alert(data.message) + }) + } +} diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 369fa97..8e26a6f 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -13,7 +13,9 @@