Monetization::Issue -> Monetization::Task

This commit is contained in:
0x1eef 2023-12-22 22:54:44 -03:00
parent 40b4553dc3
commit ccff99fb94
32 changed files with 254 additions and 254 deletions

View file

@ -42,5 +42,5 @@ class Twenty::Model < ActiveRecord::Base
connect
Twenty::Migration.run!
require_relative "model/connection"
require_relative "model/issue"
require_relative "model/task"
end

View file

@ -8,7 +8,7 @@ class Twenty::Connection < Twenty::Model
##
# Associations
has_many :issues, class_name: 'Twenty::Issue'
has_many :tasks, class_name: 'Twenty::Task'
def to_json(options = {})
{id:, name:, path:}.to_json(options)

View file

@ -1,4 +1,4 @@
class Twenty::Issue < Twenty::Model
class Twenty::Task < Twenty::Model
self.table_name = 'issues'
##

View file

@ -1,7 +1,7 @@
class Twenty::Servlet < WEBrick::HTTPServlet::AbstractServlet
require_relative "servlet/response"
require_relative "servlet/connections"
require_relative "servlet/issues"
require_relative "servlet/tasks"
def ok(res, body = {})
Response.new(res)

View file

@ -1,60 +0,0 @@
class Twenty::Servlet::Issues < Twenty::Servlet
##
# GET /servlet/issues/
# GET /servlet/issues/<id>/
def do_GET(req, res)
case req.path_info
when ""
issues = Twenty::Issue.open.order(updated_at: :desc)
ok(res, issues:)
when %r|^/([\d]+)/?$|
issue = Twenty::Issue.find_by(id: $1)
issue ? ok(res, issue:) : not_found(res)
else
not_found(res)
end
end
##
# POST /servlet/issues/
def do_POST(req, res)
case req.path_info
when ""
issue = Twenty::Issue.new(JSON.parse(req.body))
if issue.save
ok(res, issue:)
else
errors = issue.errors.full_messages
bad_request(res, errors:)
end
else
not_found(res)
end
end
##
# PUT /servlet/issues
def do_PUT(req, res)
case req.path_info
when ""
body = JSON.parse(req.body)
id = body.delete("id")
issue = Twenty::Issue.find_by(id:)
issue.update(body) ? ok(res, issue:) : not_found(res)
else
not_found(res)
end
end
##
# DELETE /servlet/issues/<id>/
def do_DELETE(req, res)
case req.path_info
when %r|^/([\d]+)/?$|
issue = Twenty::Issue.find_by(id: $1)
issue.destroy ? ok(res) : not_found(res)
else
not_found(res)
end
end
end

View file

@ -0,0 +1,60 @@
class Twenty::Servlet::Tasks < Twenty::Servlet
##
# GET /servlet/tasks/
# GET /servlet/tasks/<id>/
def do_GET(req, res)
case req.path_info
when ""
tasks = Twenty::Task.open.order(updated_at: :desc)
ok(res, tasks:)
when %r|^/([\d]+)/?$|
task = Twenty::Task.find_by(id: $1)
task ? ok(res, task:) : not_found(res)
else
not_found(res)
end
end
##
# POST /servlet/tasks/
def do_POST(req, res)
case req.path_info
when ""
task = Twenty::Task.new(JSON.parse(req.body))
if task.save
ok(res, task:)
else
errors = task.errors.full_messages
bad_request(res, errors:)
end
else
not_found(res)
end
end
##
# PUT /servlet/tasks
def do_PUT(req, res)
case req.path_info
when ""
body = JSON.parse(req.body)
id = body.delete("id")
task = Twenty::Task.find_by(id:)
task.update(body) ? ok(res, task:) : not_found(res)
else
not_found(res)
end
end
##
# DELETE /servlet/tasks/<id>/
def do_DELETE(req, res)
case req.path_info
when %r|^/([\d]+)/?$|
task = Twenty::Task.find_by(id: $1)
task.destroy ? ok(res) : not_found(res)
else
not_found(res)
end
end
end

View file

@ -12,7 +12,7 @@ class Twenty::Command::Up < Twenty::Command
def run_command
server = WEBrick::HTTPServer.new(server_options)
server.mount '/servlet/connections', Twenty::Servlet::Connections
server.mount '/servlet/issues', Twenty::Servlet::Issues
server.mount '/servlet/tasks', Twenty::Servlet::Tasks
trap(:SIGINT) { server.shutdown }
server.start
end

View file

@ -19,7 +19,7 @@ end
require_rules "nanoc/rules/assets"
require_rules "nanoc/rules/connections"
require_rules "nanoc/rules/issues"
require_rules "nanoc/rules/tasks"
compile("/**/*") { write(nil) }
layout '/**/*', :erb

View file

@ -1,48 +0,0 @@
#!/usr/bin/env ruby
##
# GET /issues/new/
compile '/html/issues/new/index.html.erb' do
layout "/default.*"
filter(:erb)
write("/issues/new/index.html")
end
##
# GET /js/main/issue/new.js
compile("/js/main/issue/new.tsx") do
filter(:webpack, depend_on: %w[/js/components/ /js/hooks/ /js/types/])
write("/js/main/issue/new.js")
end
##
# GET /
# GET /issues/
compile("/html/issues/index.html.erb") do
layout "/default.*"
filter(:erb)
write("/issues/index.html")
write("/index.html")
end
##
# GET /js/main/issues.js
compile("/js/main/issues.tsx") do
filter(:webpack, depend_on: %w[/js/components /js/hooks/ /js/types/])
write("/js/main/issues.js")
end
##
# GET /issues/edit#id=X
compile("/html/issues/edit/index.html.erb") do
layout "/default.*"
filter(:erb)
write("/issues/edit/index.html")
end
##
# GET /js/main/issue/edit.js
compile("/js/main/issue/edit.tsx") do
filter(:webpack, depend_on: %w[/js/components /js/hooks/ /js/types/])
write("/js/main/issue/edit.js")
end

View file

@ -0,0 +1,48 @@
#!/usr/bin/env ruby
##
# GET /tasks/new/
compile '/html/tasks/new/index.html.erb' do
layout "/default.*"
filter(:erb)
write("/tasks/new/index.html")
end
##
# GET /js/main/task/new.js
compile("/js/main/task/new.tsx") do
filter(:webpack, depend_on: %w[/js/components/ /js/hooks/ /js/types/])
write("/js/main/task/new.js")
end
##
# GET /
# GET /tasks/
compile("/html/tasks/index.html.erb") do
layout "/default.*"
filter(:erb)
write("/tasks/index.html")
write("/index.html")
end
##
# GET /js/main/tasks.js
compile("/js/main/tasks.tsx") do
filter(:webpack, depend_on: %w[/js/components /js/hooks/ /js/types/])
write("/js/main/tasks.js")
end
##
# GET /tasks/edit#id=X
compile("/html/tasks/edit/index.html.erb") do
layout "/default.*"
filter(:erb)
write("/tasks/edit/index.html")
end
##
# GET /js/main/task/edit.js
compile("/js/main/task/edit.tsx") do
filter(:webpack, depend_on: %w[/js/components /js/hooks/ /js/types/])
write("/js/main/task/edit.js")
end

View file

@ -36,7 +36,7 @@ body header {
.root {
color: $black;
.issue {
.task {
.content {
padding: 10px;
}

View file

@ -1,4 +0,0 @@
<div class="root align-center max-width">
<div class="react-mount edit-issue"></div>
<script src="/js/main/issue/edit.js"></script>
</div>

View file

@ -1,4 +0,0 @@
<div class="root align-center max-width">
<div class="react-mount issues"></div>
<script src="/js/main/issues.js"></script>
</div>

View file

@ -1,4 +0,0 @@
<div class="root align-center max-width">
<div class="react-mount new-issue"></div>
<script src="/js/main/issue/new.js"></script>
</div>

View file

@ -0,0 +1,4 @@
<div class="root align-center max-width">
<div class="react-mount edit-task"></div>
<script src="/js/main/task/edit.js"></script>
</div>

View file

@ -0,0 +1,4 @@
<div class="root align-center max-width">
<div class="react-mount tasks"></div>
<script src="/js/main/tasks.js"></script>
</div>

View file

@ -0,0 +1,4 @@
<div class="root align-center max-width">
<div class="react-mount new-task"></div>
<script src="/js/main/task/new.js"></script>
</div>

View file

@ -1,58 +0,0 @@
import React from "react";
import { useIssues } from "/hooks/useIssues";
import { useDestroyIssue } from "/hooks/useDestroyIssue";
import { TrashIcon, DoneIcon } from "/components/Icons";
import { DateTime } from "luxon";
import { Issue } from "/types/schema";
import { useUpsertIssue } from "hooks/useUpsertIssue";
export function Issues() {
const { issues, setIssues } = useIssues();
const upsertIssue = useUpsertIssue();
const destroyIssue = useDestroyIssue();
const onDestroy = (issue: Issue) => {
destroyIssue({id: issue.id})
.then(() => issues.filter((i) => i.id !== issue.id))
.then((issues) => setIssues(issues));
}
const onDone = (issue: Issue) => {
upsertIssue({input: {id: issue.id, state: "closed"}})
.then(() => issues.filter((i) => i.id !== issue.id))
.then((issues) => setIssues(issues));
}
return (
<div className="table">
<div className="table div">
<span>Tasks</span>
<a href="/issues/new">New task</a>
</div>
<div className="table content">
<ul className="items">
{issues.map((issue: Issue, key: number) => {
const { updated_at: updatedAt } = issue;
const datetime = DateTime.fromISO(updatedAt);
return (
<li className="item" key={key}>
<div className="top">
<a href={`/issues/edit#id=${issue.id}`}>
<span className="item title">{issue.title}</span>
</a>
<div className="actions">
<DoneIcon onClick={() => onDone(issue)} />
<TrashIcon onClick={() => onDestroy(issue)}/>
</div>
</div>
<div className="bottom">
<span>
{datetime.toFormat("dd LLL, yyyy")} at{" "}
{datetime.toFormat("HH:mm")}
</span>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
}

View file

@ -1,9 +1,9 @@
import React, { useEffect, useState, useRef } from "react";
import { useForm } from "react-hook-form";
import { Select } from "/components/forms/Select";
import { useUpsertIssue } from "/hooks/useUpsertIssue";
import { useUpsertTask } from "/hooks/useUpsertTask";
import { useConnections } from "/hooks/useConnections";
import { Issue } from "/types/schema";
import { Task } from "/types/schema";
import showdown from "showdown";
type Inputs = {
@ -13,17 +13,17 @@ type Inputs = {
connectionId: number;
};
export function Issue({ issue }: { issue?: Issue }) {
export function Task({ task }: { task?: Task }) {
const { register, handleSubmit, watch, setValue: set } = useForm<Inputs>();
const [isEditable, setIsEditable] = useState<boolean>(!issue);
const [isEditable, setIsEditable] = useState<boolean>(!task);
const selectRef = useRef<HTMLSelectElement>(null);
const upsert = useUpsertIssue();
const upsert = useUpsertTask();
const [connections] = useConnections();
const c = new showdown.Converter();
const content = watch("content");
const onSave = (input: Inputs) => {
upsert({ input }).then(() => {
location.href = "/issues/";
location.href = "/tasks/";
});
};
@ -32,8 +32,8 @@ export function Issue({ issue }: { issue?: Issue }) {
}, []);
return (
<form className="issue" onSubmit={handleSubmit(onSave)}>
<input type="hidden" value={issue?.id} {...register("id")} />
<form className="task" onSubmit={handleSubmit(onSave)}>
<input type="hidden" value={task?.id} {...register("id")} />
<div className="table">
<div className="table tabbed div">
<ul className="tabs">
@ -62,7 +62,7 @@ export function Issue({ issue }: { issue?: Issue }) {
className="form"
type="text"
placeholder="Title"
defaultValue={issue?.title}
defaultValue={task?.title}
{...register("title", { required: true })}
/>
</div>
@ -72,7 +72,7 @@ export function Issue({ issue }: { issue?: Issue }) {
<textarea
className="form"
placeholder="Add your description heren"
defaultValue={issue?.content}
defaultValue={task?.content}
{...register("content", { required: true })}
/>
</div>
@ -82,9 +82,9 @@ export function Issue({ issue }: { issue?: Issue }) {
</>
) : (
<div
className="issue content"
className="task content"
dangerouslySetInnerHTML={{
__html: c.makeHtml(content || issue?.content),
__html: c.makeHtml(content || task?.content),
}}
/>
)}

View file

@ -0,0 +1,58 @@
import React from "react";
import { useTasks } from "/hooks/useTasks";
import { useDestroyTask } from "/hooks/useDestroyTask";
import { TrashIcon, DoneIcon } from "/components/Icons";
import { DateTime } from "luxon";
import { Task } from "/types/schema";
import { useUpsertTask } from "/hooks/useUpsertTask";
export function Tasks() {
const { tasks, setTasks } = useTasks();
const upsertTask = useUpsertTask();
const destroyTask = useDestroyTask();
const onDestroy = (task: Task) => {
destroyTask({id: task.id})
.then(() => tasks.filter((t: Task) => t.id !== task.id))
.then((tasks: Task[]) => setTasks(tasks));
}
const onDone = (task: Task) => {
upsertTask({input: {id: task.id, state: "closed"}})
.then(() => tasks.filter((t: Task) => t.id !== task.id))
.then((tasks) => setTasks(tasks));
}
return (
<div className="table">
<div className="table div">
<span>Tasks</span>
<a href="/tasks/new">New task</a>
</div>
<div className="table content">
<ul className="items">
{tasks.map((task: Task, key: number) => {
const { updated_at: updatedAt } = task;
const datetime = DateTime.fromISO(updatedAt);
return (
<li className="item" key={key}>
<div className="top">
<a href={`/tasks/edit#id=${task.id}`}>
<span className="item title">{task.title}</span>
</a>
<div className="actions">
<DoneIcon onClick={() => onDone(task)} />
<TrashIcon onClick={() => onDestroy(task)}/>
</div>
</div>
<div className="bottom">
<span>
{datetime.toFormat("dd LLL, yyyy")} at{" "}
{datetime.toFormat("HH:mm")}
</span>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
}

View file

@ -2,11 +2,11 @@ type Params = {
id: number;
};
export function useDestroyIssue() {
export function useDestroyTask() {
return function ({ id }: Params) {
return new Promise((accept, reject) => {
const req = { method: "DELETE" };
return fetch(`/servlet/issues/${id}`, req)
return fetch(`/servlet/tasks/${id}`, req)
.then(res => res.json())
.then(accept)
.catch(reject);

View file

@ -1,27 +0,0 @@
import { useState, useEffect } from "react";
import { Issue } from "/types/schema";
type Result = {
setIssues: (issues: Issue[]) => unknown;
issues: Issue[];
req: () => Promise<Issue[]>;
};
export function useIssues(): Result {
const [issues, setIssues] = useState<Issue[]>([]);
const set = (ary: Issue[]) => {
setIssues(ary);
return ary;
};
const req = async function (): Promise<Issue[]> {
return await fetch("/servlet/issues")
.then((res: Response) => res.json())
.then((res: { issues: Issue[] }) => set(res.issues))
.catch(() => null);
};
useEffect(() => {
req();
}, []);
return { issues, setIssues: set, req };
}

View file

@ -0,0 +1,27 @@
import { useState, useEffect } from "react";
import { Task } from "/types/schema";
type Result = {
setTasks: (tasks: Task[]) => unknown;
tasks: Task[];
req: () => Promise<Task[]>;
};
export function useTasks(): Result {
const [tasks, setTasks] = useState<Task[]>([]);
const set = (ary: Task[]) => {
setTasks(ary);
return ary;
};
const req = async function (): Promise<Task[]> {
return await fetch("/servlet/tasks")
.then((res: Response) => res.json())
.then((res: { tasks: Task[] }) => set(res.tasks))
.catch(() => null);
};
useEffect(() => {
req();
}, []);
return { tasks, setTasks: set, req };
}

View file

@ -6,7 +6,7 @@ type Params = {
connectionId?: number;
};
export function useUpsertIssue() {
export function useUpsertTask() {
const normalize = (input: Params) => {
const { id, title, content, state, connectionId } = input;
return { id, title, content, state, connection_id: connectionId };
@ -17,7 +17,7 @@ export function useUpsertIssue() {
method: input.id ? "PUT" : "POST",
body: JSON.stringify(normalize(input)),
};
return fetch("/servlet/issues", req)
return fetch("/servlet/tasks", req)
.then(res => res.json())
.then(accept)
.catch(reject);

View file

@ -1,8 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { ReadIssue } from "/components/ReadIssue";
(function () {
const root = document.querySelector(".react-mount.edit-issue")!;
ReactDOM.createRoot(root).render(<ReadIssue />);
})();

View file

@ -1,8 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Issue } from "/components/Issue";
(function () {
const root = document.querySelector(".react-mount.new-issue")!;
ReactDOM.createRoot(root).render(<Issue />);
})();

View file

@ -1,8 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Issues } from "/components/Issues";
(function () {
const n1 = document.querySelector(".react-mount.issues")!;
ReactDOM.createRoot(n1).render(<Issues />);
})();

View file

@ -0,0 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Task } from "/components/Task";
(function () {
const root = document.querySelector(".react-mount.edit-task")!;
ReactDOM.createRoot(root).render(<Task/>);
})();

View file

@ -0,0 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Task } from "/components/Task";
(function () {
const root = document.querySelector(".react-mount.new-task")!;
ReactDOM.createRoot(root).render(<Task />);
})();

View file

@ -0,0 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Tasks } from "/components/Tasks";
(function () {
const n1 = document.querySelector(".react-mount.tasks")!;
ReactDOM.createRoot(n1).render(<Tasks />);
})();

View file

@ -4,7 +4,7 @@ export type Connection = {
path: string;
};
export type Issue = {
export type Task = {
id: number;
title: string;
content: string;

View file

@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
gem.version = "0.1.0"
gem.licenses = ["0BSD"]
gem.files = []
gem.summary = "Simple task management for hobby projects"
gem.summary = "twenty provides simple task management on http://localhost"
gem.description = gem.summary
gem.add_runtime_dependency "twenty-backend", "~> 0.1"
gem.add_runtime_dependency "twenty-frontend", "~> 0.1"