mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
Add backend
This commit is contained in:
12
backend/.gitignore
vendored
Normal file
12
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
profiles.clj
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
||||
24
backend/CHANGELOG.md
Normal file
24
backend/CHANGELOG.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- Add a new arity to `make-widget-async` to provide a different widget shape.
|
||||
|
||||
## [0.1.1] - 2020-02-20
|
||||
### Changed
|
||||
- Documentation on how to make the widgets.
|
||||
|
||||
### Removed
|
||||
- `make-widget-sync` - we're all async, all the time.
|
||||
|
||||
### Fixed
|
||||
- Fixed widget maker to keep working when daylight savings switches over.
|
||||
|
||||
## 0.1.0 - 2020-02-20
|
||||
### Added
|
||||
- Files from the new template.
|
||||
- Widget maker public API - `make-widget-sync`.
|
||||
|
||||
[Unreleased]: https://github.com/your-name/backend/compare/0.1.1...HEAD
|
||||
[0.1.1]: https://github.com/your-name/backend/compare/0.1.0...0.1.1
|
||||
277
backend/LICENSE
Normal file
277
backend/LICENSE
Normal file
@@ -0,0 +1,277 @@
|
||||
Eclipse Public License - v 2.0
|
||||
|
||||
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
|
||||
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
|
||||
|
||||
1. DEFINITIONS
|
||||
|
||||
"Contribution" means:
|
||||
|
||||
a) in the case of the initial Contributor, the initial content
|
||||
Distributed under this Agreement, and
|
||||
|
||||
b) in the case of each subsequent Contributor:
|
||||
i) changes to the Program, and
|
||||
ii) additions to the Program;
|
||||
where such changes and/or additions to the Program originate from
|
||||
and are Distributed by that particular Contributor. A Contribution
|
||||
"originates" from a Contributor if it was added to the Program by
|
||||
such Contributor itself or anyone acting on such Contributor's behalf.
|
||||
Contributions do not include changes or additions to the Program that
|
||||
are not Modified Works.
|
||||
|
||||
"Contributor" means any person or entity that Distributes the Program.
|
||||
|
||||
"Licensed Patents" mean patent claims licensable by a Contributor which
|
||||
are necessarily infringed by the use or sale of its Contribution alone
|
||||
or when combined with the Program.
|
||||
|
||||
"Program" means the Contributions Distributed in accordance with this
|
||||
Agreement.
|
||||
|
||||
"Recipient" means anyone who receives the Program under this Agreement
|
||||
or any Secondary License (as applicable), including Contributors.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source Code or other
|
||||
form, that is based on (or derived from) the Program and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship.
|
||||
|
||||
"Modified Works" shall mean any work in Source Code or other form that
|
||||
results from an addition to, deletion from, or modification of the
|
||||
contents of the Program, including, for purposes of clarity any new file
|
||||
in Source Code form that contains any contents of the Program. Modified
|
||||
Works shall not include works that contain only declarations,
|
||||
interfaces, types, classes, structures, or files of the Program solely
|
||||
in each case in order to link to, bind by name, or subclass the Program
|
||||
or Modified Works thereof.
|
||||
|
||||
"Distribute" means the acts of a) distributing or b) making available
|
||||
in any manner that enables the transfer of a copy.
|
||||
|
||||
"Source Code" means the form of a Program preferred for making
|
||||
modifications, including but not limited to software source code,
|
||||
documentation source, and configuration files.
|
||||
|
||||
"Secondary License" means either the GNU General Public License,
|
||||
Version 2.0, or any later versions of that license, including any
|
||||
exceptions or additional permissions as identified by the initial
|
||||
Contributor.
|
||||
|
||||
2. GRANT OF RIGHTS
|
||||
|
||||
a) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free copyright
|
||||
license to reproduce, prepare Derivative Works of, publicly display,
|
||||
publicly perform, Distribute and sublicense the Contribution of such
|
||||
Contributor, if any, and such Derivative Works.
|
||||
|
||||
b) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free patent
|
||||
license under Licensed Patents to make, use, sell, offer to sell,
|
||||
import and otherwise transfer the Contribution of such Contributor,
|
||||
if any, in Source Code or other form. This patent license shall
|
||||
apply to the combination of the Contribution and the Program if, at
|
||||
the time the Contribution is added by the Contributor, such addition
|
||||
of the Contribution causes such combination to be covered by the
|
||||
Licensed Patents. The patent license shall not apply to any other
|
||||
combinations which include the Contribution. No hardware per se is
|
||||
licensed hereunder.
|
||||
|
||||
c) Recipient understands that although each Contributor grants the
|
||||
licenses to its Contributions set forth herein, no assurances are
|
||||
provided by any Contributor that the Program does not infringe the
|
||||
patent or other intellectual property rights of any other entity.
|
||||
Each Contributor disclaims any liability to Recipient for claims
|
||||
brought by any other entity based on infringement of intellectual
|
||||
property rights or otherwise. As a condition to exercising the
|
||||
rights and licenses granted hereunder, each Recipient hereby
|
||||
assumes sole responsibility to secure any other intellectual
|
||||
property rights needed, if any. For example, if a third party
|
||||
patent license is required to allow Recipient to Distribute the
|
||||
Program, it is Recipient's responsibility to acquire that license
|
||||
before distributing the Program.
|
||||
|
||||
d) Each Contributor represents that to its knowledge it has
|
||||
sufficient copyright rights in its Contribution, if any, to grant
|
||||
the copyright license set forth in this Agreement.
|
||||
|
||||
e) Notwithstanding the terms of any Secondary License, no
|
||||
Contributor makes additional grants to any Recipient (other than
|
||||
those set forth in this Agreement) as a result of such Recipient's
|
||||
receipt of the Program under the terms of a Secondary License
|
||||
(if permitted under the terms of Section 3).
|
||||
|
||||
3. REQUIREMENTS
|
||||
|
||||
3.1 If a Contributor Distributes the Program in any form, then:
|
||||
|
||||
a) the Program must also be made available as Source Code, in
|
||||
accordance with section 3.2, and the Contributor must accompany
|
||||
the Program with a statement that the Source Code for the Program
|
||||
is available under this Agreement, and informs Recipients how to
|
||||
obtain it in a reasonable manner on or through a medium customarily
|
||||
used for software exchange; and
|
||||
|
||||
b) the Contributor may Distribute the Program under a license
|
||||
different than this Agreement, provided that such license:
|
||||
i) effectively disclaims on behalf of all other Contributors all
|
||||
warranties and conditions, express and implied, including
|
||||
warranties or conditions of title and non-infringement, and
|
||||
implied warranties or conditions of merchantability and fitness
|
||||
for a particular purpose;
|
||||
|
||||
ii) effectively excludes on behalf of all other Contributors all
|
||||
liability for damages, including direct, indirect, special,
|
||||
incidental and consequential damages, such as lost profits;
|
||||
|
||||
iii) does not attempt to limit or alter the recipients' rights
|
||||
in the Source Code under section 3.2; and
|
||||
|
||||
iv) requires any subsequent distribution of the Program by any
|
||||
party to be under a license that satisfies the requirements
|
||||
of this section 3.
|
||||
|
||||
3.2 When the Program is Distributed as Source Code:
|
||||
|
||||
a) it must be made available under this Agreement, or if the
|
||||
Program (i) is combined with other material in a separate file or
|
||||
files made available under a Secondary License, and (ii) the initial
|
||||
Contributor attached to the Source Code the notice described in
|
||||
Exhibit A of this Agreement, then the Program may be made available
|
||||
under the terms of such Secondary Licenses, and
|
||||
|
||||
b) a copy of this Agreement must be included with each copy of
|
||||
the Program.
|
||||
|
||||
3.3 Contributors may not remove or alter any copyright, patent,
|
||||
trademark, attribution notices, disclaimers of warranty, or limitations
|
||||
of liability ("notices") contained within the Program from any copy of
|
||||
the Program which they Distribute, provided that Contributors may add
|
||||
their own appropriate notices.
|
||||
|
||||
4. COMMERCIAL DISTRIBUTION
|
||||
|
||||
Commercial distributors of software may accept certain responsibilities
|
||||
with respect to end users, business partners and the like. While this
|
||||
license is intended to facilitate the commercial use of the Program,
|
||||
the Contributor who includes the Program in a commercial product
|
||||
offering should do so in a manner which does not create potential
|
||||
liability for other Contributors. Therefore, if a Contributor includes
|
||||
the Program in a commercial product offering, such Contributor
|
||||
("Commercial Contributor") hereby agrees to defend and indemnify every
|
||||
other Contributor ("Indemnified Contributor") against any losses,
|
||||
damages and costs (collectively "Losses") arising from claims, lawsuits
|
||||
and other legal actions brought by a third party against the Indemnified
|
||||
Contributor to the extent caused by the acts or omissions of such
|
||||
Commercial Contributor in connection with its distribution of the Program
|
||||
in a commercial product offering. The obligations in this section do not
|
||||
apply to any claims or Losses relating to any actual or alleged
|
||||
intellectual property infringement. In order to qualify, an Indemnified
|
||||
Contributor must: a) promptly notify the Commercial Contributor in
|
||||
writing of such claim, and b) allow the Commercial Contributor to control,
|
||||
and cooperate with the Commercial Contributor in, the defense and any
|
||||
related settlement negotiations. The Indemnified Contributor may
|
||||
participate in any such claim at its own expense.
|
||||
|
||||
For example, a Contributor might include the Program in a commercial
|
||||
product offering, Product X. That Contributor is then a Commercial
|
||||
Contributor. If that Commercial Contributor then makes performance
|
||||
claims, or offers warranties related to Product X, those performance
|
||||
claims and warranties are such Commercial Contributor's responsibility
|
||||
alone. Under this section, the Commercial Contributor would have to
|
||||
defend claims against the other Contributors related to those performance
|
||||
claims and warranties, and if a court requires any other Contributor to
|
||||
pay any damages as a result, the Commercial Contributor must pay
|
||||
those damages.
|
||||
|
||||
5. NO WARRANTY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
|
||||
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
|
||||
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
|
||||
TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
|
||||
PURPOSE. Each Recipient is solely responsible for determining the
|
||||
appropriateness of using and distributing the Program and assumes all
|
||||
risks associated with its exercise of rights under this Agreement,
|
||||
including but not limited to the risks and costs of program errors,
|
||||
compliance with applicable laws, damage to or loss of data, programs
|
||||
or equipment, and unavailability or interruption of operations.
|
||||
|
||||
6. DISCLAIMER OF LIABILITY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
|
||||
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
|
||||
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
|
||||
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. GENERAL
|
||||
|
||||
If any provision of this Agreement is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this Agreement, and without further
|
||||
action by the parties hereto, such provision shall be reformed to the
|
||||
minimum extent necessary to make such provision valid and enforceable.
|
||||
|
||||
If Recipient institutes patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||
Program itself (excluding combinations of the Program with other software
|
||||
or hardware) infringes such Recipient's patent(s), then such Recipient's
|
||||
rights granted under Section 2(b) shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
All Recipient's rights under this Agreement shall terminate if it
|
||||
fails to comply with any of the material terms or conditions of this
|
||||
Agreement and does not cure such failure in a reasonable period of
|
||||
time after becoming aware of such noncompliance. If all Recipient's
|
||||
rights under this Agreement terminate, Recipient agrees to cease use
|
||||
and distribution of the Program as soon as reasonably practicable.
|
||||
However, Recipient's obligations under this Agreement and any licenses
|
||||
granted by Recipient relating to the Program shall continue and survive.
|
||||
|
||||
Everyone is permitted to copy and distribute copies of this Agreement,
|
||||
but in order to avoid inconsistency the Agreement is copyrighted and
|
||||
may only be modified in the following manner. The Agreement Steward
|
||||
reserves the right to publish new versions (including revisions) of
|
||||
this Agreement from time to time. No one other than the Agreement
|
||||
Steward has the right to modify this Agreement. The Eclipse Foundation
|
||||
is the initial Agreement Steward. The Eclipse Foundation may assign the
|
||||
responsibility to serve as the Agreement Steward to a suitable separate
|
||||
entity. Each new version of the Agreement will be given a distinguishing
|
||||
version number. The Program (including Contributions) may always be
|
||||
Distributed subject to the version of the Agreement under which it was
|
||||
received. In addition, after a new version of the Agreement is published,
|
||||
Contributor may elect to Distribute the Program (including its
|
||||
Contributions) under the new version.
|
||||
|
||||
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
|
||||
receives no rights or licenses to the intellectual property of any
|
||||
Contributor under this Agreement, whether expressly, by implication,
|
||||
estoppel or otherwise. All rights in the Program not expressly granted
|
||||
under this Agreement are reserved. Nothing in this Agreement is intended
|
||||
to be enforceable by any entity that is not a Contributor or Recipient.
|
||||
No third-party beneficiary rights are created under this Agreement.
|
||||
|
||||
Exhibit A - Form of Secondary Licenses Notice
|
||||
|
||||
"This Source Code may also be made available under the following
|
||||
Secondary Licenses when the conditions for such availability set forth
|
||||
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
|
||||
version(s), and exceptions or additional permissions here}."
|
||||
|
||||
Simply including a copy of this Agreement, including this Exhibit A
|
||||
is not sufficient to license the Source Code under Secondary Licenses.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to
|
||||
look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
22
backend/README.md
Normal file
22
backend/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# backend
|
||||
|
||||
A Clojure library designed to ... well, that part is up to you.
|
||||
|
||||
## Usage
|
||||
|
||||
FIXME
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2020 FIXME
|
||||
|
||||
This program and the accompanying materials are made available under the
|
||||
terms of the Eclipse Public License 2.0 which is available at
|
||||
http://www.eclipse.org/legal/epl-2.0.
|
||||
|
||||
This Source Code may also be made available under the following Secondary
|
||||
Licenses when the conditions for such availability set forth in the Eclipse
|
||||
Public License, v. 2.0 are satisfied: GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or (at your
|
||||
option) any later version, with the GNU Classpath Exception which is available
|
||||
at https://www.gnu.org/software/classpath/license.html.
|
||||
89
backend/dev/user.clj
Normal file
89
backend/dev/user.clj
Normal file
@@ -0,0 +1,89 @@
|
||||
(ns user
|
||||
(:require [com.stuartsierra.component :as component]
|
||||
[clojure.tools.namespace.repl :as namespace]
|
||||
[backend.config :as config]
|
||||
[backend.db-migrate :as migrate]
|
||||
[io.pedestal.service-tools.dev :as dev]
|
||||
[clj-time
|
||||
[coerce :as tc]
|
||||
[core :as t]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(namespace/disable-reload!)
|
||||
(namespace/set-refresh-dirs "src" "dev")
|
||||
(defonce *system (atom nil))
|
||||
(defonce *db (atom nil))
|
||||
|
||||
(defn migrate []
|
||||
(migrate/migrate @*db))
|
||||
|
||||
(defn rollback []
|
||||
(migrate/rollback @*db))
|
||||
|
||||
(defn stop []
|
||||
(some-> @*system (component/stop))
|
||||
(reset! *system nil))
|
||||
|
||||
(defn refresh []
|
||||
(let [res (namespace/refresh)]
|
||||
(when (not= res :ok)
|
||||
(throw res))
|
||||
:ok))
|
||||
|
||||
(defn go
|
||||
[]
|
||||
(require 'backend.core)
|
||||
(dev/watch)
|
||||
(when-some [f (resolve 'backend.system/new-system)]
|
||||
(when-some [system (f config/config)]
|
||||
(when-some [system' (component/start system)]
|
||||
(reset! *system system')
|
||||
(reset! *db {:datasource (get-in @*system [:hikari :datasource])}))))
|
||||
(migrate))
|
||||
|
||||
(defn reset []
|
||||
(stop)
|
||||
(refresh)
|
||||
(go))
|
||||
|
||||
(defn get-unix-timestamp []
|
||||
(tc/to-long (t/now)))
|
||||
|
||||
(def date-format
|
||||
"Format for DateTime"
|
||||
"yyyyMMddHHmmss")
|
||||
(def migrations-dir
|
||||
"Default migrations directory"
|
||||
"resources/migrations/")
|
||||
(def ragtime-format-edn
|
||||
"EDN template for SQL migrations"
|
||||
"{:up [\"\"]\n :down [\"\"]}")
|
||||
|
||||
(defn migrations-dir-exist?
|
||||
"Checks if 'resources/migrations' directory exists"
|
||||
[]
|
||||
(.isDirectory (io/file migrations-dir)))
|
||||
|
||||
(defn now
|
||||
"Gets the current DateTime" []
|
||||
(.format (java.text.SimpleDateFormat. date-format) (new java.util.Date)))
|
||||
|
||||
(defn migration-file-path
|
||||
"Complete migration file path"
|
||||
[name]
|
||||
(str migrations-dir (now) "_" (string/replace name #"\s+|-+|_+" "_") ".edn"))
|
||||
|
||||
(defn create-migration
|
||||
"Creates a migration file with the current DateTime"
|
||||
[name]
|
||||
(let [migration-file (migration-file-path name)]
|
||||
(if-not (migrations-dir-exist?)
|
||||
(io/make-parents migration-file))
|
||||
(spit migration-file ragtime-format-edn)))
|
||||
|
||||
(defn reset-db
|
||||
[]
|
||||
(dotimes [i 100]
|
||||
(rollback))
|
||||
(migrate))
|
||||
3
backend/doc/intro.md
Normal file
3
backend/doc/intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction to backend
|
||||
|
||||
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)
|
||||
35
backend/project.clj
Normal file
35
backend/project.clj
Normal file
@@ -0,0 +1,35 @@
|
||||
(defproject backend "0.1.0-SNAPSHOT"
|
||||
:description "FIXME: write description"
|
||||
:url "http://example.com/FIXME"
|
||||
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
|
||||
:url "https://www.eclipse.org/legal/epl-2.0/"}
|
||||
:dependencies [[org.clojure/clojure "1.10.0"]
|
||||
[clj-social "0.1.5"]
|
||||
[org.postgresql/postgresql "42.2.8"]
|
||||
[org.clojure/java.jdbc "0.7.10"]
|
||||
[honeysql "0.9.8"]
|
||||
[hikari-cp "2.9.0"]
|
||||
[toucan "1.15.0"]
|
||||
[ragtime "0.8.0"]
|
||||
[com.taoensso/timbre "4.10.0"]
|
||||
[org.clojure/tools.namespace "0.3.1"]
|
||||
[buddy/buddy-sign "3.1.0"]
|
||||
[buddy/buddy-hashers "1.4.0"]
|
||||
[enlive "1.1.6"]
|
||||
[io.pedestal/pedestal.service "0.5.5"]
|
||||
[io.pedestal/pedestal.jetty "0.5.5"]
|
||||
[metosin/reitit-pedestal "0.4.2"]
|
||||
[metosin/reitit "0.4.2"]
|
||||
[metosin/jsonista "0.2.5"]
|
||||
[aero "1.1.6"]
|
||||
[com.stuartsierra/component "0.4.0"]
|
||||
]
|
||||
;; :main backend.core
|
||||
:profiles {:repl {:dependencies [[io.pedestal/pedestal.service-tools "0.5.7"]]
|
||||
:source-paths ["src/backend" "dev"]}
|
||||
:uberjar {:main backend.core
|
||||
:aot :all}}
|
||||
:repl-options {:init-ns user}
|
||||
:jvm-opts ["-Duser.timezone=UTC" "-Dclojure.spec.check-asserts=true"]
|
||||
:aliases {"migrate" ["run" "-m" "user/migrate"]
|
||||
"rollback" ["run" "-m" "user/rollback"]})
|
||||
23
backend/resources/config.edn
Normal file
23
backend/resources/config.edn
Normal file
@@ -0,0 +1,23 @@
|
||||
{:env #or [#env ENVIRONMENT "dev"]
|
||||
:port #or [#env PORT 8080]
|
||||
:oauth {:github {:app-key #env GITHUB_APP_KEY
|
||||
:app-secret #env GITHUB_APP_SECRET}}
|
||||
:jwt-secret #env JWT_SECRET
|
||||
:cookie-secret #env COOKIE_SECRET
|
||||
:log-path #or [#env LOG_PATH "/tmp/gitnotes"]
|
||||
:hikari-spec {:auto-commit true
|
||||
:read-only false
|
||||
:connection-timeout 30000
|
||||
:validation-timeout 5000
|
||||
:idle-timeout 600000
|
||||
:max-lifetime 1800000
|
||||
:minimum-idle 10
|
||||
:maximum-pool-size 48
|
||||
:pool-name "gitnotes-clj-db-pool"
|
||||
:adapter "postgresql"
|
||||
:username #env PG_USERNAME
|
||||
:password #env PG_PASSWORD
|
||||
:database-name "gitnotes"
|
||||
:server-name "localhost"
|
||||
:port-number 5432
|
||||
:register-mbeans false}}
|
||||
52
backend/resources/logback.xml
Normal file
52
backend/resources/logback.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->
|
||||
<!-- Scanning is currently turned on; This will impact performance! -->
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<!-- Silence Logback's own status messages about config parsing
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" /> -->
|
||||
|
||||
<!-- Simple file output -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %X{io.pedestal} - %msg%n</pattern>
|
||||
</encoder>
|
||||
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<!-- rollover daily -->
|
||||
<fileNamePattern>logs/restream-api-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<!-- or whenever the file size reaches 64 MB -->
|
||||
<maxFileSize>64 MB</maxFileSize>
|
||||
</rollingPolicy>
|
||||
|
||||
<!-- Safely log to the same file from multiple JVMs. Degrades performance! -->
|
||||
<prudent>true</prudent>
|
||||
</appender>
|
||||
|
||||
|
||||
<!-- Console output -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||
<encoder>
|
||||
<pattern>%-5level %logger{36} %X{io.pedestal} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<!-- Only log level INFO and above -->
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
|
||||
<!-- Enable FILE and STDOUT appenders for all log messages.
|
||||
By default, only log at level INFO and above. -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="FILE" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
<!-- For loggers in the these namespaces, log at all levels. -->
|
||||
<logger name="user" level="ALL" />
|
||||
<!-- To log pedestal internals, enable this and change ThresholdFilter to DEBUG
|
||||
<logger name="io.pedestal" level="ALL" />
|
||||
-->
|
||||
|
||||
</configuration>
|
||||
24
backend/src/backend/components/hikari.clj
Normal file
24
backend/src/backend/components/hikari.clj
Normal file
@@ -0,0 +1,24 @@
|
||||
(ns backend.components.hikari
|
||||
(:require [com.stuartsierra.component :as component]
|
||||
[hikari-cp.core :as hikari]
|
||||
[clojure.java.jdbc :as j]
|
||||
[toucan.db :as toucan]
|
||||
[backend.db-migrate :as migrate]))
|
||||
|
||||
(defrecord Hikari [db-spec datasource]
|
||||
component/Lifecycle
|
||||
(start [component]
|
||||
(let [s (or datasource (hikari/make-datasource db-spec))]
|
||||
;; set time zone
|
||||
(j/execute! {:datasource s} ["set time zone 'UTC'"])
|
||||
;; migrate
|
||||
(migrate/migrate {:datasource s})
|
||||
(toucan/set-default-db-connection! {:datasource s})
|
||||
(assoc component :datasource s)))
|
||||
(stop [component]
|
||||
(when datasource
|
||||
(hikari/close-datasource datasource))
|
||||
(assoc component :datasource nil)))
|
||||
|
||||
(defn new-hikari-cp [db-spec]
|
||||
(map->Hikari {:db-spec db-spec}))
|
||||
26
backend/src/backend/components/http.clj
Normal file
26
backend/src/backend/components/http.clj
Normal file
@@ -0,0 +1,26 @@
|
||||
(ns backend.components.http
|
||||
(:require [com.stuartsierra.component :as component]
|
||||
[io.pedestal.http :as http]))
|
||||
|
||||
(defn test?
|
||||
[service-map]
|
||||
(= :test (:env service-map)))
|
||||
|
||||
(defrecord Server [service-map service]
|
||||
component/Lifecycle
|
||||
(start [this]
|
||||
(prn "service-map: " service-map)
|
||||
(if service
|
||||
this
|
||||
(cond-> service-map
|
||||
true http/create-server
|
||||
(not (test? service-map)) http/start
|
||||
true ((partial assoc this :service)))))
|
||||
(stop [this]
|
||||
(when (and service (not (test? service-map)))
|
||||
(http/stop service))
|
||||
(assoc this :service nil)))
|
||||
|
||||
(defn new-server
|
||||
[]
|
||||
(map->Server {}))
|
||||
12
backend/src/backend/config.clj
Normal file
12
backend/src/backend/config.clj
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns backend.config
|
||||
(:require [aero.core :refer (read-config)]
|
||||
[clojure.java.io :as io]))
|
||||
|
||||
(def config (read-config (io/resource "config.edn")))
|
||||
|
||||
(def production? (= "production" (:env config)))
|
||||
(def dev? (= "dev" (:env config)))
|
||||
(def test? (= "test" (:env config)))
|
||||
(def website-uri (if dev?
|
||||
"http://localhost:8080"
|
||||
"https://gitnotes.com"))
|
||||
58
backend/src/backend/cookie.clj
Normal file
58
backend/src/backend/cookie.clj
Normal file
@@ -0,0 +1,58 @@
|
||||
(ns backend.cookie
|
||||
(:require [buddy.sign.compact :as buddy]
|
||||
[backend.util :as util]
|
||||
[backend.config :as config]))
|
||||
|
||||
(defn sign [token]
|
||||
(buddy/sign token (:cookie-secret config/config)))
|
||||
|
||||
(defn unsign [cookie]
|
||||
(buddy/unsign cookie (:cookie-secret config/config)))
|
||||
|
||||
;; domain path expires
|
||||
(defn token-cookie [value & {:keys [max-age path]
|
||||
:or {path "/"
|
||||
max-age (* (* 3600 24) 30)}}]
|
||||
(let [dev? config/dev?
|
||||
xsrf-token (str (util/uuid))
|
||||
domain (if-not dev?
|
||||
".chengdongchengxi.com"
|
||||
"")
|
||||
secure (if-not dev?
|
||||
true
|
||||
false)]
|
||||
{"x" (cond->
|
||||
{:value (sign value)
|
||||
:max-age max-age
|
||||
:http-only true
|
||||
:path path
|
||||
:secure secure}
|
||||
domain
|
||||
(assoc :domain domain))
|
||||
"xsrf-token" (cond->
|
||||
{:value xsrf-token
|
||||
:max-age max-age
|
||||
:http-only true
|
||||
:path "/"
|
||||
:secure secure}
|
||||
domain
|
||||
(assoc :domain domain))}))
|
||||
|
||||
(def delete-token
|
||||
(let [domain (if-not config/dev?
|
||||
".chengdongchengxi.com"
|
||||
"")]
|
||||
{"x" {:value ""
|
||||
:path "/"
|
||||
:expires "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
:http-only true
|
||||
:domain domain}
|
||||
"xsrf-token" {:value ""
|
||||
:path "/"
|
||||
:expires "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
:http-only true
|
||||
:domain domain}}))
|
||||
|
||||
(defn get-token [req]
|
||||
(when-let [access-token (get-in req [:cookies "x" :value])]
|
||||
(unsign access-token)))
|
||||
21
backend/src/backend/core.clj
Normal file
21
backend/src/backend/core.clj
Normal file
@@ -0,0 +1,21 @@
|
||||
(ns backend.core
|
||||
(:require [backend.config :as config]
|
||||
[backend.system :as system]
|
||||
[taoensso.timbre :as timbre]
|
||||
[taoensso.timbre.appenders.core :as appenders]
|
||||
[com.stuartsierra.component :as component]))
|
||||
|
||||
(defn set-logger!
|
||||
[log-path]
|
||||
(timbre/merge-config! (cond->
|
||||
{:appenders {:spit (appenders/spit-appender {:fname log-path})}}
|
||||
config/production?
|
||||
(assoc :output-fn (partial timbre/default-output-fn {:stacktrace-fonts {}})))))
|
||||
|
||||
(defn start []
|
||||
(System/setProperty "https.protocols" "TLSv1.2,TLSv1.1,SSLv3")
|
||||
(set-logger! (:log-path config/config))
|
||||
|
||||
(let [system (system/new-system config/config)]
|
||||
(component/start system))
|
||||
(println "server running in port 3000"))
|
||||
16
backend/src/backend/db_migrate.clj
Normal file
16
backend/src/backend/db_migrate.clj
Normal file
@@ -0,0 +1,16 @@
|
||||
(ns backend.db-migrate
|
||||
(:require [ragtime.jdbc :as jdbc]
|
||||
[ragtime.repl :as repl]))
|
||||
|
||||
;; db migrations
|
||||
(defn load-config
|
||||
[db]
|
||||
{:datastore (jdbc/sql-database db)
|
||||
:migrations (jdbc/load-resources "migrations")})
|
||||
|
||||
(defn migrate [db]
|
||||
(prn "db: " db)
|
||||
(repl/migrate (load-config db)))
|
||||
|
||||
(defn rollback [db]
|
||||
(repl/rollback (load-config db)))
|
||||
23
backend/src/backend/jwt.clj
Normal file
23
backend/src/backend/jwt.clj
Normal file
@@ -0,0 +1,23 @@
|
||||
(ns api.jwt
|
||||
(:require [buddy.sign.jwt :as jwt]
|
||||
[clj-time.core :as time]
|
||||
[backend.config :refer [config]]))
|
||||
|
||||
(defonce secret (:jwt-secret config))
|
||||
|
||||
(defn sign
|
||||
"Serialize and sign a token with defined claims"
|
||||
([m]
|
||||
(sign m (* 60 60 12)))
|
||||
([m expire-secs]
|
||||
(let [claims (assoc m
|
||||
:exp (time/plus (time/now) (time/seconds expire-secs)))]
|
||||
(jwt/sign claims secret))))
|
||||
|
||||
(defn unsign
|
||||
[token]
|
||||
(jwt/unsign token secret))
|
||||
|
||||
(defn unsign-skip-validation
|
||||
[token]
|
||||
(jwt/unsign token secret {:skip-validation true}))
|
||||
103
backend/src/backend/system.clj
Normal file
103
backend/src/backend/system.clj
Normal file
@@ -0,0 +1,103 @@
|
||||
(ns backend.system
|
||||
(:require [io.pedestal.http :as server]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.http :as http]
|
||||
[reitit.coercion.spec]
|
||||
[reitit.swagger :as swagger]
|
||||
[reitit.swagger-ui :as swagger-ui]
|
||||
[reitit.http.coercion :as coercion]
|
||||
[reitit.dev.pretty :as pretty]
|
||||
[reitit.http.interceptors.parameters :as parameters]
|
||||
[reitit.http.interceptors.muuntaja :as muuntaja]
|
||||
[reitit.http.interceptors.exception :as exception]
|
||||
[reitit.http.interceptors.multipart :as multipart]
|
||||
[reitit.http.interceptors.dev :as dev]
|
||||
[reitit.http.spec :as spec]
|
||||
[spec-tools.spell :as spell]
|
||||
[io.pedestal.http :as server]
|
||||
[reitit.pedestal :as pedestal]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.java.io :as io]
|
||||
[muuntaja.core :as m]
|
||||
[com.stuartsierra.component :as component]
|
||||
[backend.components.http :as component-http]
|
||||
[backend.components.hikari :as hikari]))
|
||||
|
||||
(def router
|
||||
(pedestal/routing-interceptor
|
||||
(http/router
|
||||
[["/swagger.json"
|
||||
{:get {:no-doc true
|
||||
:swagger {:info {:title "gitnotes api"
|
||||
:description "with pedestal & reitit-http"}}
|
||||
:handler (swagger/create-swagger-handler)}}]
|
||||
|
||||
["/login"
|
||||
{:swagger {:tags ["Login"]}}
|
||||
|
||||
["/github"
|
||||
{:get {:summary "Login with github"
|
||||
:swagger {:produces ["image/png"]}
|
||||
:handler (fn [_]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "image/png"}
|
||||
:body (io/input-stream
|
||||
(io/resource "reitit.png"))})}}]] ]
|
||||
|
||||
{;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
|
||||
;;:validate spec/validate ;; enable spec validation for route data
|
||||
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
|
||||
:exception pretty/exception
|
||||
:data {:coercion reitit.coercion.spec/coercion
|
||||
:muuntaja m/instance
|
||||
:interceptors [;; swagger feature
|
||||
swagger/swagger-feature
|
||||
;; query-params & form-params
|
||||
(parameters/parameters-interceptor)
|
||||
;; content-negotiation
|
||||
(muuntaja/format-negotiate-interceptor)
|
||||
;; encoding response body
|
||||
(muuntaja/format-response-interceptor)
|
||||
;; exception handling
|
||||
(exception/exception-interceptor)
|
||||
;; decoding request body
|
||||
(muuntaja/format-request-interceptor)
|
||||
;; coercing response bodys
|
||||
(coercion/coerce-response-interceptor)
|
||||
;; coercing request parameters
|
||||
(coercion/coerce-request-interceptor)
|
||||
;; multipart
|
||||
(multipart/multipart-interceptor)]}})
|
||||
|
||||
;; optional default ring handler (if no routes have matched)
|
||||
(ring/routes
|
||||
(swagger-ui/create-swagger-ui-handler
|
||||
{:path "/"
|
||||
:config {:validatorUrl nil
|
||||
:operationsSorter "alpha"}})
|
||||
(ring/create-resource-handler)
|
||||
(ring/create-default-handler))))
|
||||
|
||||
(defn new-system
|
||||
[{:keys [env port hikari-spec] :as config}]
|
||||
(let [service-map (-> {:env env
|
||||
::server/type :jetty
|
||||
::server/port port
|
||||
::server/join? false
|
||||
;; no pedestal routes
|
||||
::server/routes []
|
||||
;; allow serving the swagger-ui styles & scripts from self
|
||||
::server/secure-headers {:content-security-policy-settings
|
||||
{:default-src "'self'"
|
||||
:style-src "'self' 'unsafe-inline'"
|
||||
:script-src "'self' 'unsafe-inline'"}}}
|
||||
(server/default-interceptors)
|
||||
;; use the reitit router
|
||||
(pedestal/replace-last-interceptor router)
|
||||
(server/dev-interceptors))]
|
||||
(component/system-map :service-map service-map
|
||||
:hikari (hikari/new-hikari-cp hikari-spec)
|
||||
:http
|
||||
(component/using
|
||||
(component-http/new-server)
|
||||
[:service-map]))))
|
||||
82
backend/src/backend/util.clj
Normal file
82
backend/src/backend/util.clj
Normal file
@@ -0,0 +1,82 @@
|
||||
(ns api.util
|
||||
(:require [clojure.string :as str]
|
||||
[clj-time
|
||||
[coerce :as tc]
|
||||
[core :as t]
|
||||
[format :as tf]])
|
||||
(:import [java.util UUID]
|
||||
[java.util TimerTask Timer]))
|
||||
(defn uuid
|
||||
"Generate uuid."
|
||||
[]
|
||||
(UUID/randomUUID))
|
||||
|
||||
(defn ->uuid
|
||||
[s]
|
||||
(if (uuid? s)
|
||||
s
|
||||
(UUID/fromString s)))
|
||||
|
||||
(defn update-if
|
||||
"Update m if k exists."
|
||||
[m k f]
|
||||
(if-let [v (get m k)]
|
||||
(assoc m k (f v))
|
||||
m))
|
||||
|
||||
(defn dissoc-in
|
||||
"Dissociates an entry from a nested associative structure returning a new
|
||||
nested structure. keys is a sequence of keys. Any empty maps that result
|
||||
will not be present in the new structure."
|
||||
[m [k & ks :as keys]]
|
||||
(if ks
|
||||
(if-let [nextmap (get m k)]
|
||||
(let [newmap (dissoc-in nextmap ks)]
|
||||
(if (seq newmap)
|
||||
(assoc m k newmap)
|
||||
(dissoc m k)))
|
||||
m)
|
||||
(dissoc m k)))
|
||||
|
||||
(defmacro doseq-indexed
|
||||
"loops over a set of values, binding index-sym to the 0-based index of each value"
|
||||
([[val-sym values index-sym] & code]
|
||||
`(loop [vals# (seq ~values)
|
||||
~index-sym (long 0)]
|
||||
(if vals#
|
||||
(let [~val-sym (first vals#)]
|
||||
~@code
|
||||
(recur (next vals#) (inc ~index-sym)))
|
||||
nil))))
|
||||
|
||||
(defn indexed [coll] (map-indexed vector coll))
|
||||
|
||||
(defn set-timeout [f interval]
|
||||
(let [task (proxy [TimerTask] []
|
||||
(run [] (f)))
|
||||
timer (new Timer)]
|
||||
(.schedule timer task (long interval))
|
||||
timer))
|
||||
|
||||
;; http://yellerapp.com/posts/2014-12-11-14-race-condition-in-clojure-println.html
|
||||
(defn safe-println [& more]
|
||||
(.write *out* (str (clojure.string/join " " more) "\n")))
|
||||
|
||||
(defn safe->int
|
||||
[s]
|
||||
(if (string? s)
|
||||
(Integer/parseInt s)
|
||||
s))
|
||||
|
||||
(defn remove-nils
|
||||
[m]
|
||||
(reduce (fn [acc [k v]] (if v (assoc acc k v)
|
||||
acc))
|
||||
{} m))
|
||||
|
||||
(defn deep-merge [& maps]
|
||||
(apply merge-with (fn [& args]
|
||||
(if (every? map? args)
|
||||
(apply deep-merge args)
|
||||
(last args)))
|
||||
maps))
|
||||
7
backend/test/backend/core_test.clj
Normal file
7
backend/test/backend/core_test.clj
Normal file
@@ -0,0 +1,7 @@
|
||||
(ns backend.core-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[backend.core :refer :all]))
|
||||
|
||||
(deftest a-test
|
||||
(testing "FIXME, I fail."
|
||||
(is (= 0 1))))
|
||||
21
frontend/.gitignore
vendored
Normal file
21
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
node_modules/
|
||||
public/js
|
||||
|
||||
/.cpcache
|
||||
/target
|
||||
/checkouts
|
||||
/src/gen
|
||||
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.iml
|
||||
*.jar
|
||||
*.log
|
||||
.shadow-cljs
|
||||
.idea
|
||||
.lein-*
|
||||
.nrepl-*
|
||||
.DS_Store
|
||||
|
||||
.hgignore
|
||||
.hg/
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
98
frontend/src/frontend/components/agenda.cljs
Normal file
98
frontend/src/frontend/components/agenda.cljs
Normal file
@@ -0,0 +1,98 @@
|
||||
(ns frontend.components.agenda
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.mui :as mui]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.format.org.block :as block]
|
||||
[frontend.state :as state]
|
||||
[clojure.string :as string]
|
||||
[frontend.format.org-mode :as org]))
|
||||
|
||||
(rum/defc timestamps-cp
|
||||
[timestamps]
|
||||
[:ul
|
||||
(for [[type {:keys [date time]}] timestamps]
|
||||
(let [{:keys [year month day]} date
|
||||
{:keys [hour min]} time]
|
||||
[:li {:key type}
|
||||
[:span {:style {:margin-right 6}} type]
|
||||
[:span (if time
|
||||
(str year "-" month "-" day " " hour ":" min)
|
||||
(str year "-" month "-" day))]]))])
|
||||
|
||||
(rum/defc title-cp
|
||||
[title]
|
||||
(let [title-json (js/JSON.stringify (clj->js title))
|
||||
html (org/inline-list->html title-json)]
|
||||
(util/raw-html html)))
|
||||
|
||||
(rum/defc marker-cp
|
||||
[marker]
|
||||
[:span {:class (str "marker-" (string/lower-case marker))
|
||||
:style {:margin-left 8}}
|
||||
(if (contains? #{"DOING" "IN-PROGRESS"} marker)
|
||||
(str " (" marker ")"))])
|
||||
|
||||
(rum/defc tags-cp
|
||||
[tags]
|
||||
[:span
|
||||
(for [tag tags]
|
||||
[:span.tag {:key tag}
|
||||
[:span
|
||||
tag]])])
|
||||
|
||||
(rum/defc agenda
|
||||
[tasks]
|
||||
[:span "TBD"]
|
||||
;; [:div#agenda
|
||||
;; (if (seq tasks)
|
||||
;; (for [[section-name tasks] tasks]
|
||||
;; [:div.section {:key (str "section-" section-name)}
|
||||
;; [:h3 section-name]
|
||||
;; (mui/list
|
||||
;; (for [[idx {:keys [marker title priority level tags children timestamps meta]}] (util/indexed (block/sort-tasks tasks))]
|
||||
;; (mui/list-item
|
||||
;; {:key (str "task-" section-name "-" idx)
|
||||
;; :style {:padding-left 8
|
||||
;; :padding-right 8}}
|
||||
;; [:div.column
|
||||
;; [:div.row {:style {:align-items "center"}}
|
||||
;; (let [marker (case marker
|
||||
;; (list "DOING" "IN-PROGRESS" "TODO")
|
||||
;; (mui/checkbox {:checked false
|
||||
;; :on-change (fn [_]
|
||||
;; ;; FIXME: Log timestamp
|
||||
;; (handler/check marker (:pos meta)))
|
||||
;; :color "primary"
|
||||
;; :style {:padding 0}})
|
||||
|
||||
;; "WAIT"
|
||||
;; [:span {:style {:font-weight "bold"}}
|
||||
;; "WAIT"]
|
||||
|
||||
;; "DONE"
|
||||
;; (mui/checkbox {:checked true
|
||||
;; :on-change (fn [_]
|
||||
;; ;; FIXME: rollback to the last state if exists.
|
||||
;; ;; it must not be `TODO`
|
||||
;; (handler/uncheck (:pos meta)))
|
||||
;; :color "primary"
|
||||
;; :style {:padding 0}})
|
||||
|
||||
;; nil)]
|
||||
;; (if priority
|
||||
;; (mui/badge {:badge-content (string/lower-case priority)
|
||||
;; :overlay "circle"}
|
||||
;; marker)
|
||||
;; marker))
|
||||
|
||||
;; [:div.row {:style {:margin-left 8}}
|
||||
;; (title-cp title)
|
||||
;; (marker-cp marker)
|
||||
;; (when (seq tags)
|
||||
;; (tags-cp tags))]]
|
||||
;; (when (seq timestamps)
|
||||
;; (timestamps-cp timestamps))
|
||||
;; ])))])
|
||||
;; "Empty")]
|
||||
)
|
||||
66
frontend/src/frontend/components/file.cljs
Normal file
66
frontend/src/frontend/components/file.cljs
Normal file
@@ -0,0 +1,66 @@
|
||||
(ns frontend.components.file
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.mui :as mui]
|
||||
["@material-ui/core/colors" :as colors]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(rum/defc files-list
|
||||
[files]
|
||||
[:div
|
||||
(if (seq files)
|
||||
(let [files-set (set files)
|
||||
prefix [(files-set "tasks.org") (files-set "links.org")]
|
||||
files (->> (remove (set prefix) files)
|
||||
(concat prefix)
|
||||
(remove nil?))]
|
||||
(mui/list
|
||||
(for [file files]
|
||||
(mui/list-item
|
||||
{:button true
|
||||
:key file
|
||||
:style {:overflow "hidden"}
|
||||
:on-click (fn []
|
||||
(handler/load-file file)
|
||||
(handler/toggle-drawer? false))}
|
||||
(mui/list-item-text file)))))
|
||||
"Loading...")])
|
||||
|
||||
(rum/defc edit < rum/reactive
|
||||
[]
|
||||
(let [state (rum/react state/state)
|
||||
{:keys [current-file contents]} state]
|
||||
(mui/container
|
||||
{:id "root-container"
|
||||
:style {:display "flex"
|
||||
:justify-content "center"
|
||||
:margin-top 64}}
|
||||
[:div.column
|
||||
(let [paths [:editing-files current-file]]
|
||||
(mui/textarea {:style {:margin-bottom 12
|
||||
:padding 8
|
||||
:min-height 300}
|
||||
:auto-focus true
|
||||
:on-change (fn [event]
|
||||
(let [v (util/evalue event)]
|
||||
(swap! state/state assoc-in paths v)))
|
||||
:default-value (get contents current-file)
|
||||
:value (get-in state/state paths)}))
|
||||
(let [path [:commit-message current-file]]
|
||||
(mui/text-field {:id "standard-basic"
|
||||
:style {:margin-bottom 12}
|
||||
:label "Commit message"
|
||||
:auto-focus true
|
||||
:on-change (fn [event]
|
||||
(let [v (util/evalue event)]
|
||||
(when-not (string/blank? v)
|
||||
(swap! state/state assoc-in path v))))
|
||||
:default-value (str "Update " current-file)
|
||||
:value (get-in state/state path)}))
|
||||
(mui/button {:variant "contained"
|
||||
:color "primary"
|
||||
:on-click (fn []
|
||||
(handler/alter-file current-file))}
|
||||
"Submit")])))
|
||||
73
frontend/src/frontend/components/home.cljs
Normal file
73
frontend/src/frontend/components/home.cljs
Normal file
@@ -0,0 +1,73 @@
|
||||
(ns frontend.components.home
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.mui :as mui]
|
||||
["@material-ui/core/colors" :as colors]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.components.agenda :as agenda]
|
||||
[frontend.components.file :as file]
|
||||
[frontend.components.settings :as settings]
|
||||
[frontend.format :as format]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(rum/defc content-html
|
||||
< {:did-mount (fn [state]
|
||||
(doseq [block (-> (js/document.querySelectorAll "pre code")
|
||||
(array-seq))]
|
||||
(js/hljs.highlightBlock block))
|
||||
state)}
|
||||
[current-file html-content]
|
||||
[:div
|
||||
(mui/link {:style {:float "right"}
|
||||
:on-click (fn []
|
||||
(handler/change-page :edit-file))}
|
||||
"edit")
|
||||
(util/raw-html html-content)])
|
||||
|
||||
(rum/defc home < rum/reactive
|
||||
[]
|
||||
(let [state (rum/react state/state)
|
||||
{:keys [cloned? github-username github-token github-repo contents loadings current-file files width drawer? tasks links cloning?]} state
|
||||
loading? (get loadings current-file)
|
||||
width (or width (util/get-width))
|
||||
mobile? (and width (<= width 600))]
|
||||
(mui/container
|
||||
{:id "root-container"
|
||||
:style {:display "flex"
|
||||
:justify-content "center"
|
||||
;; TODO: fewer spacing for mobile, 24px
|
||||
:margin-top 64}}
|
||||
(cond
|
||||
cloned?
|
||||
(mui/grid
|
||||
{:container true
|
||||
:spacing 3}
|
||||
(when-not mobile?
|
||||
(mui/grid {:xs 2}
|
||||
(file/files-list files)))
|
||||
|
||||
(if (and (not mobile?)
|
||||
(not drawer?))
|
||||
(mui/divider {:orientation "vertical"
|
||||
:style {:margin "0 24px"}}))
|
||||
(mui/grid {:xs 9
|
||||
:style {:margin-left (if (or mobile? drawer?) 24 0)}}
|
||||
(cond
|
||||
(nil? current-file)
|
||||
(agenda/agenda tasks)
|
||||
|
||||
loading?
|
||||
[:div "Loading ..."]
|
||||
|
||||
:else
|
||||
(let [content (get contents current-file)
|
||||
suffix (last (string/split current-file #"\."))]
|
||||
(if (and suffix (contains? #{"md" "markdown" "org"} suffix))
|
||||
(content-html current-file (format/to-html content suffix))
|
||||
[:div "File " suffix " is not supported."])))))
|
||||
cloning?
|
||||
[:div "Cloning..."]
|
||||
|
||||
:else
|
||||
(settings/settings-form github-username github-token github-repo)))))
|
||||
58
frontend/src/frontend/components/link.cljs
Normal file
58
frontend/src/frontend/components/link.cljs
Normal file
@@ -0,0 +1,58 @@
|
||||
(ns frontend.components.link
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.mui :as mui]
|
||||
[frontend.util :as util]
|
||||
[frontend.state :as state]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(rum/defc links < rum/reactive
|
||||
[]
|
||||
(let [state (rum/react state/state)
|
||||
links (reverse (get state :links))]
|
||||
(mui/container
|
||||
{:id "root-container"
|
||||
:style {:display "flex"
|
||||
:justify-content "center"
|
||||
;; TODO: fewer spacing for mobile, 24px
|
||||
:margin-top 64}}
|
||||
(if (seq links)
|
||||
(mui/list
|
||||
(for [[idx link] (util/indexed links)]
|
||||
(mui/list-item
|
||||
{:key (str "link-" idx)}
|
||||
(mui/list-item-text
|
||||
[:a {:href link
|
||||
:target "_blank"}
|
||||
link]))))
|
||||
[:div "Loading..."]))))
|
||||
|
||||
(rum/defcs dialog < (rum/local "" :link)
|
||||
[state open?]
|
||||
(let [link (get state :link)]
|
||||
(mui/dialog
|
||||
{:open open?
|
||||
:on-close (fn []
|
||||
(handler/toggle-link-dialog? false))}
|
||||
(mui/dialog-title "Add new link")
|
||||
(mui/dialog-content
|
||||
(mui/text-field
|
||||
{:auto-focus true
|
||||
:auto-complete "off"
|
||||
:margin "dense"
|
||||
:id "link"
|
||||
:label "Link"
|
||||
:full-width true
|
||||
:value @link
|
||||
:on-change (fn [e] (reset! link (util/evalue e)))}))
|
||||
(mui/dialog-actions
|
||||
(mui/button {:on-click (fn []
|
||||
(handler/toggle-link-dialog? false))
|
||||
:color "primary"}
|
||||
"Cancel")
|
||||
(mui/button {:on-click (fn []
|
||||
(when-not (string/blank? @link)
|
||||
(handler/add-new-link @link
|
||||
"New link")))
|
||||
:color "primary"}
|
||||
"Submit")))))
|
||||
65
frontend/src/frontend/components/settings.cljs
Normal file
65
frontend/src/frontend/components/settings.cljs
Normal file
@@ -0,0 +1,65 @@
|
||||
(ns frontend.components.settings
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.mui :as mui]
|
||||
[frontend.util :as util]
|
||||
[frontend.state :as state]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(defn settings-form
|
||||
[github-username github-token github-repo]
|
||||
[:form {:style {:min-width 300}}
|
||||
(mui/grid
|
||||
{:container true
|
||||
:direction "column"}
|
||||
(mui/text-field {:id "standard-basic"
|
||||
:style {:margin-bottom 12}
|
||||
:label "Github username"
|
||||
:auto-focus true
|
||||
:on-change (fn [event]
|
||||
(let [v (util/evalue event)]
|
||||
(swap! state/state assoc :github-username v)))
|
||||
:value github-username})
|
||||
(mui/text-field {:id "standard-basic"
|
||||
:style {:margin-bottom 12}
|
||||
:label "Github repo"
|
||||
:on-change (fn [event]
|
||||
(let [v (util/evalue event)]
|
||||
(swap! state/state assoc :github-repo v)))
|
||||
:value github-repo
|
||||
})
|
||||
(mui/text-field {:id "standard-basic"
|
||||
:style {:margin-bottom 12}
|
||||
:label "Github basic token"
|
||||
:on-change (fn [event]
|
||||
(let [v (util/evalue event)]
|
||||
(swap! state/state assoc :github-token v)))
|
||||
:value github-token})
|
||||
(mui/button {:variant "contained"
|
||||
:color "primary"
|
||||
:on-click (fn []
|
||||
(when (and github-token github-repo)
|
||||
(handler/clone github-username github-token github-repo)))}
|
||||
"Sync"))])
|
||||
|
||||
(rum/defc settings < rum/reactive
|
||||
[]
|
||||
;; Change username, repo and basic token
|
||||
(let [state (rum/react state/state)
|
||||
{:keys [github-username github-token github-repo]} state]
|
||||
(mui/container
|
||||
{:id "root-container"
|
||||
:style {:display "flex"
|
||||
:justify-content "center"
|
||||
:margin-top 64}}
|
||||
|
||||
[:div
|
||||
|
||||
(settings-form github-username github-token github-repo)
|
||||
|
||||
(mui/divider {:style {:margin "24px 0"}})
|
||||
|
||||
;; clear storage
|
||||
(mui/button {:on-click handler/clear-storage
|
||||
:color "primary"}
|
||||
"Clear storage and clone")])))
|
||||
7
frontend/src/frontend/config.cljs
Normal file
7
frontend/src/frontend/config.cljs
Normal file
@@ -0,0 +1,7 @@
|
||||
(ns frontend.config)
|
||||
|
||||
(defonce dir "/gitnotes")
|
||||
|
||||
(defonce tasks-org "tasks.org")
|
||||
(defonce links-org "links.org")
|
||||
(defonce hidden-file ".hidden")
|
||||
38
frontend/src/frontend/core.cljs
Normal file
38
frontend/src/frontend/core.cljs
Normal file
@@ -0,0 +1,38 @@
|
||||
(ns frontend.core
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.git :as git]
|
||||
[frontend.fs :as fs]
|
||||
[frontend.util :as util]
|
||||
[frontend.state :as state]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.routes :as routes]
|
||||
[frontend.page :as page]))
|
||||
|
||||
(defn start []
|
||||
(rum/mount (page/current-page)
|
||||
(.getElementById js/document "root")))
|
||||
|
||||
(defn ^:export init []
|
||||
;; init is called ONCE when the page loads
|
||||
;; this is called in the index.html and must be exported
|
||||
;; so it is available even in :advanced release builds
|
||||
|
||||
(handler/load-from-disk)
|
||||
|
||||
(when (:cloned? @state/state)
|
||||
(handler/initial-db!)
|
||||
(handler/periodically-pull)
|
||||
(handler/periodically-push-tasks))
|
||||
|
||||
(handler/listen-to-resize)
|
||||
|
||||
(handler/request-notifications-if-not-asked)
|
||||
|
||||
(handler/run-notify-worker!)
|
||||
|
||||
(start))
|
||||
|
||||
(defn stop []
|
||||
;; stop is called before any code is reloaded
|
||||
;; this is controlled by :before-load in the config
|
||||
(js/console.log "stop"))
|
||||
176
frontend/src/frontend/db.cljs
Normal file
176
frontend/src/frontend/db.cljs
Normal file
@@ -0,0 +1,176 @@
|
||||
(ns frontend.db
|
||||
(:require [datascript.core :as d]
|
||||
[frontend.util :as util]
|
||||
[medley.core :as medley]))
|
||||
|
||||
(def conn (d/create-conn))
|
||||
|
||||
;; links
|
||||
[:link/id
|
||||
:link/label
|
||||
:link/link]
|
||||
|
||||
;; TODO: added_at, started_at, schedule, deadline
|
||||
(def qualified-map
|
||||
{:file :heading/file
|
||||
:anchor :heading/anchor
|
||||
:title :heading/title
|
||||
:marker :heading/marker
|
||||
:priority :heading/priority
|
||||
:level :heading/level
|
||||
:timestamps :heading/timestamps
|
||||
:children :heading/children
|
||||
:tags :heading/tags
|
||||
:meta :heading/meta
|
||||
:parent-title :heading/parent-title})
|
||||
|
||||
(def schema
|
||||
[{:db/ident :heading/uuid
|
||||
:db/valueType :db.type/uuid
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db/unique :db.unique/value}
|
||||
|
||||
{:db/ident :heading/file
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :heading/anchor
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :heading/marker
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :heading/priority
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :heading/level
|
||||
:db/valueType :db.type/long
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :heading/tags
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/many
|
||||
:db/isComponent true} ;TODO: not working as Datomic, can't search :tag/name in datalog queries
|
||||
|
||||
{:db/ident :task/scheduled
|
||||
;; :db/valueType :db.type/string
|
||||
:db/index true}
|
||||
|
||||
{:db/ident :task/deadline
|
||||
;; :db/valueType :db.type/string
|
||||
:db/index true}
|
||||
|
||||
{:db/ident :tag/name
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db/unique :db.unique/identity}
|
||||
|
||||
;; {:db/ident :heading/title
|
||||
;; :db/valueType :db.type/string
|
||||
;; :db/cardinality :db.cardinality/one}
|
||||
|
||||
;; {:db/ident :heading/parent-title
|
||||
;; :db/valueType :db.type/string
|
||||
;; :db/cardinality :db.cardinality/one}
|
||||
|
||||
;; TODO: timestamps, meta
|
||||
;; scheduled, deadline
|
||||
])
|
||||
|
||||
(defn ->tags
|
||||
[tags]
|
||||
(map (fn [tag]
|
||||
{:db/id tag
|
||||
:tag/name tag})
|
||||
tags))
|
||||
|
||||
(defn extract-timestamps
|
||||
[{:keys [meta] :as heading}]
|
||||
(let [{:keys [pos timestamps]} meta]
|
||||
))
|
||||
|
||||
(defn- safe-headings
|
||||
[headings]
|
||||
(mapv (fn [heading]
|
||||
(let [heading (-> (util/remove-nils heading)
|
||||
(assoc :heading/uuid (d/squuid)))
|
||||
heading (assoc heading :tags
|
||||
(->tags (:tags heading)))]
|
||||
(medley/map-keys
|
||||
(fn [k] (get qualified-map k k))
|
||||
heading)))
|
||||
headings))
|
||||
|
||||
(defn init
|
||||
[]
|
||||
(d/transact! conn [{:tx-data schema}]))
|
||||
|
||||
;; transactions
|
||||
(defn transact-headings!
|
||||
[headings]
|
||||
(prn "headings: " headings)
|
||||
(let [headings (safe-headings headings)]
|
||||
(d/transact! conn headings)))
|
||||
|
||||
;; queries
|
||||
|
||||
(defn- distinct-result
|
||||
[query-result]
|
||||
(-> query-result
|
||||
seq
|
||||
flatten
|
||||
distinct))
|
||||
|
||||
(def seq-flatten (comp flatten seq))
|
||||
|
||||
(defn get-all-tags
|
||||
[]
|
||||
(distinct-result
|
||||
(d/q '[:find ?tags
|
||||
:where
|
||||
[?h :heading/tags ?tags]]
|
||||
@conn)))
|
||||
|
||||
(defn get-all-headings
|
||||
[]
|
||||
(seq-flatten
|
||||
(d/q '[:find (pull ?h [*])
|
||||
:where
|
||||
[?h :heading/title]]
|
||||
@conn)))
|
||||
|
||||
;; marker should be one of: TODO, DOING, IN-PROGRESS
|
||||
;; time duration
|
||||
(defn get-agenda
|
||||
[time]
|
||||
(let [duration (case time
|
||||
:today []
|
||||
:week []
|
||||
:month [])]
|
||||
(d/q '[:find (pull ?h [*])
|
||||
:where
|
||||
(or [?h :heading/marker "TODO"]
|
||||
[?h :heading/marker "DOING"]
|
||||
[?h :heading/marker "IN-PROGRESS"])]
|
||||
@conn)))
|
||||
|
||||
(defn search-headings-by-title
|
||||
[title])
|
||||
|
||||
(defn get-headings-by-tag
|
||||
[tag]
|
||||
(let [pred (fn [db tags]
|
||||
(some #(= tag %) tags))]
|
||||
(d/q '[:find (flatten (pull ?h [*]))
|
||||
:in $ ?pred
|
||||
:where
|
||||
[?h :heading/tags ?tags]
|
||||
[(?pred $ ?tags)]]
|
||||
@conn pred)))
|
||||
|
||||
(comment
|
||||
(frontend.handler/initial-db!)
|
||||
)
|
||||
14
frontend/src/frontend/format.cljs
Normal file
14
frontend/src/frontend/format.cljs
Normal file
@@ -0,0 +1,14 @@
|
||||
(ns frontend.format
|
||||
(:require [frontend.format.org-mode :as org :refer [->OrgMode]]
|
||||
[frontend.format.markdown :as markdown :refer [->Markdown]]
|
||||
[frontend.format.protocol :as protocol]))
|
||||
|
||||
(defn to-html
|
||||
[content suffix]
|
||||
(when-let [record (case suffix
|
||||
"org"
|
||||
(->OrgMode content)
|
||||
(list "md" "markdown")
|
||||
(->Markdown content)
|
||||
nil)]
|
||||
(protocol/toHtml record)))
|
||||
10
frontend/src/frontend/format/markdown.cljs
Normal file
10
frontend/src/frontend/format/markdown.cljs
Normal file
@@ -0,0 +1,10 @@
|
||||
(ns frontend.format.markdown
|
||||
(:require ["showdown" :refer [Converter]]
|
||||
[frontend.format.protocol :as protocol]))
|
||||
|
||||
(defonce converter (Converter.))
|
||||
|
||||
(defrecord Markdown [content]
|
||||
protocol/Format
|
||||
(toHtml [this]
|
||||
(.makeHtml converter content)))
|
||||
113
frontend/src/frontend/format/org/block.cljs
Normal file
113
frontend/src/frontend/format/org/block.cljs
Normal file
@@ -0,0 +1,113 @@
|
||||
(ns frontend.format.org.block
|
||||
(:require [frontend.util :as util]))
|
||||
|
||||
(defn- heading-block?
|
||||
[block]
|
||||
(and
|
||||
(vector? block)
|
||||
(= "Heading" (first block))))
|
||||
|
||||
(defn- task-block?
|
||||
[block]
|
||||
(and
|
||||
(heading-block? block)
|
||||
(some? (:marker (second block)))))
|
||||
|
||||
;; FIXME:
|
||||
(defn extract-title
|
||||
[block]
|
||||
(-> (:title (second block))
|
||||
first
|
||||
second))
|
||||
|
||||
(defn- paragraph-block?
|
||||
[block]
|
||||
(and
|
||||
(vector? block)
|
||||
(= "Paragraph" (first block))))
|
||||
|
||||
(defn- timestamp-block?
|
||||
[block]
|
||||
(and
|
||||
(vector? block)
|
||||
(= "Timestamp" (first block))))
|
||||
|
||||
(defn- paragraph-timestamp-block?
|
||||
[block]
|
||||
(and (paragraph-block? block)
|
||||
(timestamp-block? (first (second block)))))
|
||||
|
||||
(defn extract-timestamp
|
||||
[block]
|
||||
(-> block
|
||||
second
|
||||
first
|
||||
second))
|
||||
|
||||
(defn extract-headings
|
||||
[blocks]
|
||||
[blocks]
|
||||
(let [reversed-blocks (reverse blocks)]
|
||||
(loop [child-level 0
|
||||
current-heading-children []
|
||||
children-headings []
|
||||
result []
|
||||
rblocks reversed-blocks
|
||||
timestamps {}]
|
||||
(if (seq rblocks)
|
||||
(let [block (first rblocks)
|
||||
level (:level (second block))]
|
||||
(cond
|
||||
(and (>= level child-level) (heading-block? block))
|
||||
(let [heading (assoc (second block)
|
||||
:children (reverse current-heading-children)
|
||||
:timestamps timestamps)
|
||||
children-headings (conj children-headings heading)]
|
||||
(recur level [] children-headings result (rest rblocks) {}))
|
||||
|
||||
(paragraph-timestamp-block? block)
|
||||
(let [timestamp (extract-timestamp block)
|
||||
timestamps' (conj timestamps timestamp)]
|
||||
(recur child-level current-heading-children children-headings result (rest rblocks) timestamps'))
|
||||
|
||||
:else
|
||||
(let [children (conj current-heading-children block)]
|
||||
(if (and level (< level child-level))
|
||||
(let [parent-title (extract-title block)
|
||||
children-headings (map (fn [heading]
|
||||
(assoc heading :parent-title parent-title))
|
||||
children-headings)
|
||||
result (concat result children-headings)]
|
||||
(recur 0 children [] result (rest rblocks) timestamps))
|
||||
(recur child-level children children-headings result (rest rblocks) timestamps)))))
|
||||
(reverse result)))))
|
||||
|
||||
(defn get-sections
|
||||
[section-headings]
|
||||
(map first section-headings))
|
||||
|
||||
(defn get-section-headings
|
||||
[section-name section-headings]
|
||||
(-> (util/find-first
|
||||
(fn [[name headings]]
|
||||
(= name section-name))
|
||||
section-headings)
|
||||
second))
|
||||
|
||||
;; marker: DOING | IN-PROGRESS > TODO > WAITING | WAIT > DONE > CANCELED | CANCELLED
|
||||
;; priority: A > B > C
|
||||
(defn sort-tasks
|
||||
[headings]
|
||||
(let [markers ["DOING" "IN-PROGRESS" "TODO" "WAITING" "WAIT" "DONE" "CANCELED" "CANCELLED"]
|
||||
markers (zipmap markers (reverse (range 1 (count markers))))
|
||||
priorities ["A" "B" "C" "D" "E" "F" "G"]
|
||||
priorities (zipmap priorities (reverse (range 1 (count priorities))))]
|
||||
(sort (fn [t1 t2]
|
||||
(let [m1 (get markers (:marker t1) 0)
|
||||
m2 (get markers (:marker t2) 0)
|
||||
p1 (get priorities (:priority t1) 0)
|
||||
p2 (get priorities (:priority t2) 0)]
|
||||
(if (= m1 m2)
|
||||
(> p1 p2)
|
||||
(> m1 m2))))
|
||||
headings)))
|
||||
29
frontend/src/frontend/format/org_mode.cljs
Normal file
29
frontend/src/frontend/format/org_mode.cljs
Normal file
@@ -0,0 +1,29 @@
|
||||
(ns frontend.format.org-mode
|
||||
(:require ["mldoc_org" :as org]
|
||||
[frontend.format.protocol :as protocol]))
|
||||
|
||||
(defrecord OrgMode [content]
|
||||
protocol/Format
|
||||
(toHtml [this]
|
||||
(.parseHtml (.-MldocOrg org) content)))
|
||||
|
||||
(defn parse-json
|
||||
[content]
|
||||
(.parseJson (.-MldocOrg org) content))
|
||||
|
||||
(defn json->ast
|
||||
[json]
|
||||
(.jsonToAst (.-MldocOrg org) json))
|
||||
|
||||
(defn json->html
|
||||
[json]
|
||||
(.jsonToHtmlStr (.-MldocOrg org) json))
|
||||
|
||||
(defn inline-list->html
|
||||
[json]
|
||||
(.inlineListToHtmlStr (.-MldocOrg org) json))
|
||||
|
||||
(comment
|
||||
(let [text "*** TODO /*great*/ [[https://personal.utdallas.edu/~gupta/courses/acl/papers/datalog-paper.pdf][What You Always Wanted to Know About Datalog]] :datalog:"
|
||||
blocks-json (parse-json text)]
|
||||
(json->html blocks-json)))
|
||||
4
frontend/src/frontend/format/protocol.cljs
Normal file
4
frontend/src/frontend/format/protocol.cljs
Normal file
@@ -0,0 +1,4 @@
|
||||
(ns frontend.format.protocol)
|
||||
|
||||
(defprotocol Format
|
||||
(toHtml [this]))
|
||||
19
frontend/src/frontend/fs.cljs
Normal file
19
frontend/src/frontend/fs.cljs
Normal file
@@ -0,0 +1,19 @@
|
||||
(ns frontend.fs
|
||||
(:require [frontend.config :refer [dir]]))
|
||||
|
||||
(defn mkdir
|
||||
[]
|
||||
(js/pfs.mkdir dir))
|
||||
|
||||
(defn readdir
|
||||
[]
|
||||
(js/pfs.readdir dir))
|
||||
|
||||
(defn read-file
|
||||
[path]
|
||||
(js/pfs.readFile (str dir "/" path)
|
||||
(clj->js {:encoding "utf8"})))
|
||||
|
||||
(defn write-file
|
||||
[path content]
|
||||
(js/pfs.writeFile (str dir "/" path) content))
|
||||
67
frontend/src/frontend/git.cljs
Normal file
67
frontend/src/frontend/git.cljs
Normal file
@@ -0,0 +1,67 @@
|
||||
(ns frontend.git
|
||||
(:refer-clojure :exclude [clone])
|
||||
(:require [promesa.core :as p]
|
||||
[frontend.util :as util]
|
||||
[frontend.config :refer [dir]]))
|
||||
|
||||
(defn clone
|
||||
[username token repo]
|
||||
(js/git.clone (clj->js
|
||||
{:dir dir
|
||||
:url repo
|
||||
:corsProxy "https://cors.isomorphic-git.org"
|
||||
:singleBranch true
|
||||
:depth 1
|
||||
:username username
|
||||
:token token
|
||||
})))
|
||||
|
||||
(defn list-files
|
||||
[]
|
||||
(js/git.listFiles (clj->js
|
||||
{:dir dir
|
||||
:ref "HEAD"})))
|
||||
|
||||
(defn pull
|
||||
[username token]
|
||||
(js/git.pull (clj->js
|
||||
{:dir dir
|
||||
:ref "master"
|
||||
:username username
|
||||
:token token
|
||||
:singleBranch true})))
|
||||
(defn add
|
||||
[file]
|
||||
(js/git.add (clj->js
|
||||
{:dir dir
|
||||
:filepath file})))
|
||||
|
||||
(defn commit
|
||||
[message]
|
||||
(js/git.commit (clj->js
|
||||
{:dir dir
|
||||
:author {:name "Orgnote"
|
||||
:email "orgnote@hello.world"}
|
||||
:message message})))
|
||||
|
||||
(defn push
|
||||
[token]
|
||||
(js/git.push (clj->js
|
||||
{:dir dir
|
||||
:remote "origin"
|
||||
:ref "master"
|
||||
:token token})))
|
||||
|
||||
(defn add-commit-push
|
||||
[file message token push-ok-handler push-error-handler]
|
||||
(util/p-handle
|
||||
(let [files (if (coll? file) file [file])]
|
||||
(doseq [file files]
|
||||
(add file)))
|
||||
(fn [_]
|
||||
(util/p-handle
|
||||
(commit message)
|
||||
(fn [_]
|
||||
(push token)
|
||||
(push-ok-handler))
|
||||
push-error-handler))))
|
||||
370
frontend/src/frontend/handler.cljs
Normal file
370
frontend/src/frontend/handler.cljs
Normal file
@@ -0,0 +1,370 @@
|
||||
(ns frontend.handler
|
||||
(:refer-clojure :exclude [clone load-file])
|
||||
(:require [frontend.git :as git]
|
||||
[frontend.fs :as fs]
|
||||
[frontend.state :as state]
|
||||
[frontend.db :as db]
|
||||
[frontend.storage :as storage]
|
||||
[frontend.util :as util]
|
||||
[frontend.format.org-mode :as org]
|
||||
[frontend.format.org.block :as block]
|
||||
[frontend.config :as config]
|
||||
[clojure.walk :as walk]
|
||||
[clojure.string :as string]
|
||||
[promesa.core :as p])
|
||||
(:import [goog.events EventHandler]))
|
||||
|
||||
(defn load-file
|
||||
([path]
|
||||
(util/p-handle (fs/read-file path)
|
||||
(fn [content]
|
||||
(let [state @state/state
|
||||
state' (-> state
|
||||
(assoc-in [:contents path] content)
|
||||
(assoc-in [:loadings path] false)
|
||||
(assoc :current-file path))]
|
||||
(reset! state/state state')))))
|
||||
([path state-handler]
|
||||
(util/p-handle (fs/read-file path)
|
||||
(fn [content]
|
||||
(state-handler content)))))
|
||||
|
||||
(defn- hidden?
|
||||
[path patterns]
|
||||
(some (fn [pattern]
|
||||
(or
|
||||
(= path pattern)
|
||||
(and (string/starts-with? pattern "/")
|
||||
(= (str "/" (first (string/split path #"/")))
|
||||
pattern)))) patterns))
|
||||
|
||||
(defn load-files
|
||||
[]
|
||||
(util/p-handle (git/list-files)
|
||||
(fn [files]
|
||||
(when (> (count files) 0)
|
||||
(let [files (js->clj files)]
|
||||
(if (contains? (set files) config/hidden-file)
|
||||
(load-file config/hidden-file
|
||||
(fn [patterns-content]
|
||||
(let [patterns (string/split patterns-content #"\n")
|
||||
files (remove (fn [path] (hidden? path patterns)) files)]
|
||||
(swap! state/state
|
||||
assoc :files files))))
|
||||
(swap! state/state
|
||||
assoc :files files)))))))
|
||||
|
||||
(defn extract-links
|
||||
[form]
|
||||
(let [links (atom [])]
|
||||
(clojure.walk/postwalk
|
||||
(fn [x]
|
||||
(when (and (vector? x)
|
||||
(= "Link" (first x)))
|
||||
(let [[_ {:keys [url label]}] x
|
||||
[_ {:keys [protocol link]}] url
|
||||
link (str protocol ":" link)]
|
||||
(swap! links conj link)))
|
||||
x)
|
||||
form)
|
||||
@links))
|
||||
|
||||
(defn load-links
|
||||
([]
|
||||
(load-links config/links-org))
|
||||
([path]
|
||||
(util/p-handle (fs/read-file path)
|
||||
(fn [content]
|
||||
(when content
|
||||
(let [blocks (org/parse-json content)
|
||||
blocks (-> (.parse js/JSON blocks)
|
||||
(js->clj :keywordize-keys true))]
|
||||
(when (seq blocks)
|
||||
(swap! state/state assoc :links (extract-links blocks)))))))))
|
||||
|
||||
(defn load-from-disk
|
||||
[]
|
||||
(let [cloned? (storage/get :cloned?)]
|
||||
(swap! state/state assoc
|
||||
:cloned? cloned?
|
||||
:github-username (storage/get :github-username)
|
||||
:github-token (storage/get :github-token)
|
||||
:github-repo (storage/get :github-repo))
|
||||
(when cloned?
|
||||
(load-files)
|
||||
(load-links))))
|
||||
|
||||
(defn periodically-pull
|
||||
[]
|
||||
(let [username (storage/get :github-username)
|
||||
token (storage/get :github-token)
|
||||
pull (fn []
|
||||
(util/p-handle (git/pull username token)
|
||||
(fn [_result]
|
||||
;; TODO: diff
|
||||
(load-files)))
|
||||
(load-links))]
|
||||
(pull)
|
||||
(js/setInterval pull
|
||||
(* 60 1000))))
|
||||
|
||||
(defn add-transaction
|
||||
[tx]
|
||||
(swap! state/state update :tasks-transactions conj tx))
|
||||
|
||||
(defn clear-transactions!
|
||||
[]
|
||||
(swap! state/state assoc :tasks-transactions nil))
|
||||
|
||||
(defn- transactions->commit-msg
|
||||
[transactions]
|
||||
(let [transactions (reverse transactions)]
|
||||
(str
|
||||
"Orgnote auto save tasks.\n\n"
|
||||
(string/join "\n" transactions))))
|
||||
|
||||
(defn periodically-push-tasks
|
||||
[]
|
||||
(let [github-token (storage/get :github-token)
|
||||
push (fn []
|
||||
(let [transactions (:tasks-transactions @state/state)]
|
||||
(when (seq transactions)
|
||||
(git/add-commit-push
|
||||
config/tasks-org
|
||||
(transactions->commit-msg transactions)
|
||||
github-token
|
||||
(fn []
|
||||
(prn "Commit tasks to Github.")
|
||||
(clear-transactions!))
|
||||
(fn []
|
||||
(prn "Failed to push."))))))]
|
||||
(js/setInterval push
|
||||
(* 5 1000))))
|
||||
|
||||
(defn clone
|
||||
[github-username github-token github-repo]
|
||||
(storage/set :github-username github-username)
|
||||
(storage/set :github-token github-token)
|
||||
(storage/set :github-repo github-repo)
|
||||
|
||||
(util/p-handle
|
||||
(do
|
||||
(swap! state/state assoc
|
||||
:cloning? true)
|
||||
(git/clone github-username github-token github-repo))
|
||||
(fn []
|
||||
(swap! state/state assoc
|
||||
:cloned? true)
|
||||
(storage/set :cloned? true)
|
||||
(swap! state/state assoc
|
||||
:cloning? false)
|
||||
(periodically-pull))
|
||||
(fn [e]
|
||||
(prn "Clone failed, reason: " e))))
|
||||
|
||||
(defonce event-handler (EventHandler.))
|
||||
|
||||
(defn listen-to-resize
|
||||
[]
|
||||
(util/listen event-handler js/window :resize
|
||||
(fn []
|
||||
(swap! state/state assoc :width (util/get-width)))))
|
||||
|
||||
(defn toggle-drawer?
|
||||
[switch]
|
||||
(swap! state/state assoc :drawer? switch))
|
||||
|
||||
(defn change-page
|
||||
[page]
|
||||
(swap! state/state assoc :current-page page))
|
||||
|
||||
(defn reset-current-file
|
||||
[]
|
||||
(swap! state/state assoc :current-file nil))
|
||||
|
||||
(defn toggle-link-dialog?
|
||||
[switch]
|
||||
(swap! state/state assoc :add-link-dialog? switch))
|
||||
|
||||
(defn add-new-link
|
||||
[link message]
|
||||
(if-let [github-token (storage/get :github-token)]
|
||||
(util/p-handle (fs/read-file config/links-org)
|
||||
(fn [content]
|
||||
(let [content' (str content "\n** " link)]
|
||||
(util/p-handle
|
||||
(fs/write-file config/links-org content')
|
||||
(fn [_]
|
||||
(git/add-commit-push config/links-org
|
||||
message
|
||||
github-token
|
||||
(fn []
|
||||
(toggle-link-dialog? false))
|
||||
(fn []
|
||||
(.log js/console "Failed to push the new link."))))))))
|
||||
(.log js/console "Github token does not exists!")))
|
||||
|
||||
(defn new-notification
|
||||
[text]
|
||||
(js/Notification. "Gitnotes" #js {:body text
|
||||
;; :icon logo
|
||||
}))
|
||||
|
||||
(defn request-notifications
|
||||
[]
|
||||
(util/p-handle (.requestPermission js/Notification)
|
||||
(fn [result]
|
||||
(storage/set :notification-permission-asked? true)
|
||||
|
||||
(when (= "granted" result)
|
||||
(storage/set :notification-permission? true)))))
|
||||
|
||||
(defn request-notifications-if-not-asked
|
||||
[]
|
||||
(when-not (storage/get :notification-permission-asked?)
|
||||
(request-notifications)))
|
||||
|
||||
;; notify deadline or scheduled tasks
|
||||
(defn run-notify-worker!
|
||||
[]
|
||||
(when (storage/get :notification-permission?)
|
||||
(let [notify-fn (fn []
|
||||
(let [tasks (:tasks @state/state)
|
||||
tasks (flatten (vals tasks))]
|
||||
(doseq [{:keys [marker title] :as task} tasks]
|
||||
(when-not (contains? #{"DONE" "CANCElED" "CANCELLED"} marker)
|
||||
(doseq [[type {:keys [date time] :as timestamp}] (:timestamps task)]
|
||||
(let [{:keys [year month day]} date
|
||||
{:keys [hour min]
|
||||
:or {hour 9
|
||||
min 0}} time
|
||||
now (util/get-local-date)]
|
||||
(when (and (contains? #{"Scheduled" "Deadline"} type)
|
||||
(= (assoc date :hour hour :minute min) now))
|
||||
(let [notification-text (str type ": " (second (first title)))]
|
||||
(new-notification notification-text)))))))))]
|
||||
(notify-fn)
|
||||
(js/setInterval notify-fn (* 1000 60)))))
|
||||
|
||||
(defn hide-snackbar
|
||||
[]
|
||||
(swap! state/state assoc
|
||||
:snackbar? false
|
||||
:snackbar-message nil))
|
||||
|
||||
(defn show-snackbar
|
||||
[message]
|
||||
(swap! state/state assoc
|
||||
:snackbar? true
|
||||
:snackbar-message message)
|
||||
(js/setTimeout hide-snackbar 3000))
|
||||
|
||||
(defn alter-file
|
||||
[file]
|
||||
(when-let [content (get-in @state/state [:contents file])]
|
||||
(let [content' (get-in @state/state [:editing-files file])]
|
||||
(when-not (= (string/trim content)
|
||||
(string/trim content'))
|
||||
(let [github-token (:github-token @state/state)
|
||||
path [:commit-message file]
|
||||
message (get-in @state/state path (str "Update " file))]
|
||||
(util/p-handle
|
||||
(fs/write-file file content')
|
||||
(fn [_]
|
||||
(git/add-commit-push file
|
||||
message
|
||||
github-token
|
||||
(fn []
|
||||
(swap! state/state util/dissoc-in path)
|
||||
(swap! state/state assoc-in [:contents file] content')
|
||||
(show-snackbar "File updated!")
|
||||
(change-page :home))
|
||||
(fn []
|
||||
(prn "Failed to update file."))))))))))
|
||||
|
||||
(defn clear-storage
|
||||
[]
|
||||
(js/window.pfs._idb.wipe)
|
||||
(storage/set :cloned? false)
|
||||
(swap! state/state assoc
|
||||
:cloned? false
|
||||
:contents nil
|
||||
:files nil)
|
||||
(clone (:github-username @state/state)
|
||||
(:github-token @state/state)
|
||||
(:github-repo @state/state)))
|
||||
|
||||
(defn check
|
||||
[marker pos]
|
||||
(let [file config/tasks-org
|
||||
github-token (storage/get :github-token)]
|
||||
(when-let [content (get-in @state/state [:contents file])]
|
||||
(let [content' (str (subs content 0 pos)
|
||||
(-> (subs content pos)
|
||||
(string/replace-first marker "DONE")))]
|
||||
;; TODO: optimize, only update the specific block
|
||||
;; (build-tasks content' file)
|
||||
(util/p-handle
|
||||
(fs/write-file file content')
|
||||
(fn [_]
|
||||
(swap! state/state assoc-in [:contents file] content')
|
||||
(add-transaction (util/format "`%s` marked as DONE." marker))))))))
|
||||
|
||||
(defn uncheck
|
||||
[pos]
|
||||
(let [file config/tasks-org
|
||||
github-token (storage/get :github-token)]
|
||||
(when-let [content (get-in @state/state [:contents file])]
|
||||
(let [content' (str (subs content 0 pos)
|
||||
(-> (subs content pos)
|
||||
(string/replace-first "DONE" "TODO")))]
|
||||
;; TODO: optimize, only update the specific block
|
||||
;; (build-tasks content' file)
|
||||
(util/p-handle
|
||||
(fs/write-file file content')
|
||||
(fn [_]
|
||||
(swap! state/state assoc-in [:contents file] content')
|
||||
(add-transaction "DONE rollbacks to TODO.")))))))
|
||||
|
||||
(defn extract-headings
|
||||
[file content]
|
||||
(let [headings (-> content
|
||||
(org/parse-json)
|
||||
(util/json->clj))
|
||||
headings (block/extract-headings headings)]
|
||||
(map (fn [heading]
|
||||
(assoc heading :file file))
|
||||
headings)))
|
||||
|
||||
(defn load-all-contents!
|
||||
[ok-handler]
|
||||
(let [files (:files @state/state)]
|
||||
(-> (p/all (for [file files]
|
||||
(load-file file
|
||||
(fn [content]
|
||||
(swap! state/state
|
||||
assoc-in [:contents file] content) ))))
|
||||
(p/then
|
||||
(fn [_]
|
||||
(prn "Files are loaded!")
|
||||
(ok-handler))))))
|
||||
|
||||
(defn extract-all-headings
|
||||
[]
|
||||
(let [contents (:contents @state/state)]
|
||||
(vec
|
||||
(mapcat
|
||||
(fn [[file content] contents]
|
||||
(extract-headings file content))
|
||||
contents))))
|
||||
|
||||
(defonce headings-atom (atom nil))
|
||||
|
||||
(defn initial-db!
|
||||
[]
|
||||
(db/init)
|
||||
(load-all-contents!
|
||||
(fn []
|
||||
(let [headings (extract-all-headings)]
|
||||
(reset! headings-atom headings)
|
||||
(db/transact-headings! headings)))))
|
||||
71
frontend/src/frontend/layout.cljs
Normal file
71
frontend/src/frontend/layout.cljs
Normal file
@@ -0,0 +1,71 @@
|
||||
(ns frontend.layout
|
||||
(:require [frontend.mui :as mui]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.state :as state]
|
||||
[frontend.components.link :as link]
|
||||
[frontend.components.file :as file]
|
||||
[rum.core :as rum]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(rum/defc frame < rum/reactive
|
||||
[content width link-dialog?]
|
||||
(let [state (rum/react state/state)
|
||||
{:keys [files drawer? snackbar? snackbar-message]} state
|
||||
mobile? (and width (<= width 600))]
|
||||
(mui/theme-provider
|
||||
{:theme (mui/custom-theme)}
|
||||
[:div {:class "root"
|
||||
:style {:padding-bottom 100}}
|
||||
(mui/css-baseline)
|
||||
(mui/app-bar
|
||||
{:position "static"}
|
||||
(mui/tool-bar
|
||||
{}
|
||||
(if mobile?
|
||||
(mui/icon-button {:edge "start"
|
||||
:class "menuButton"
|
||||
:color "inherit"
|
||||
:on-click (fn []
|
||||
(handler/toggle-drawer? true))}
|
||||
(mui/menu-icon)))
|
||||
(mui/typography {:class "grow"
|
||||
:variant "h6"
|
||||
:color "inherit"
|
||||
:no-wrap true
|
||||
:on-click (fn []
|
||||
(handler/change-page :home)
|
||||
(handler/reset-current-file))}
|
||||
"Gitnotes")
|
||||
|
||||
(mui/button {:color "inherit"
|
||||
:on-click (fn []
|
||||
(handler/change-page :links))}
|
||||
"Links")
|
||||
|
||||
(mui/button {:color "inherit"
|
||||
:on-click (fn []
|
||||
(handler/change-page :settings))}
|
||||
"Settings")
|
||||
|
||||
(mui/icon-button {:color "inherit"
|
||||
:class "addButton"
|
||||
:on-click (fn []
|
||||
(handler/toggle-link-dialog? true))}
|
||||
(mui/add-icon))))
|
||||
content
|
||||
|
||||
(if mobile?
|
||||
(mui/drawer {:open drawer?
|
||||
:disableBackdropTransition true
|
||||
:on-open (fn []
|
||||
(handler/toggle-drawer? true))
|
||||
:on-close (fn []
|
||||
(handler/toggle-drawer? false))}
|
||||
[:div {:style {:width 240}}
|
||||
(file/files-list files)]))
|
||||
|
||||
(link/dialog link-dialog?)
|
||||
|
||||
(mui/snackbar {:open snackbar?
|
||||
:auto-hide-duration 3000
|
||||
:message snackbar-message})])))
|
||||
112
frontend/src/frontend/mui.cljs
Normal file
112
frontend/src/frontend/mui.cljs
Normal file
@@ -0,0 +1,112 @@
|
||||
(ns frontend.mui
|
||||
(:refer-clojure :exclude [list stepper])
|
||||
(:require [rum.core]
|
||||
[frontend.rum :as r]
|
||||
["@material-ui/core" :refer [MuiThemeProvider]]
|
||||
["@material-ui/core/styles" :refer [createMuiTheme withStyles makeStyles]]
|
||||
["@material-ui/core/colors" :as colors]
|
||||
["@material-ui/core/CssBaseline" :default CssBaseline]
|
||||
["@material-ui/core/Typography" :default Typography]
|
||||
["@material-ui/core/Avatar" :default mui-avatar]
|
||||
["@material-ui/icons/Android" :default AndroidIcon]
|
||||
["@material-ui/core/AppBar" :default AppBar]
|
||||
["@material-ui/core/Divider" :default Divider]
|
||||
["@material-ui/core/Paper" :default Paper]
|
||||
["@material-ui/core/Toolbar" :default ToolBar]
|
||||
["@material-ui/core/IconButton" :default IconButton]
|
||||
["@material-ui/icons/Menu" :default MenuIcon]
|
||||
["@material-ui/core/Button" :default Button]
|
||||
["@material-ui/core/SwipeableDrawer" :default SwipeableDrawer]
|
||||
["@material-ui/core/Chip" :default Chip]
|
||||
["@material-ui/core/Fab" :default Fab]
|
||||
["@material-ui/core/List" :default List]
|
||||
["@material-ui/core/ListItem" :default ListItem]
|
||||
["@material-ui/core/ListItemText" :default ListItemText]
|
||||
["@material-ui/core/Container" :default Container]
|
||||
["@material-ui/core/Box" :default Box]
|
||||
["@material-ui/core/Snackbar" :default Snackbar]
|
||||
["@material-ui/core/Link" :default Link]
|
||||
["@material-ui/core/Checkbox" :default Checkbox]
|
||||
["@material-ui/core/Grid" :default Grid]
|
||||
["@material-ui/core/GridList" :default GridList]
|
||||
["@material-ui/core/Hidden" :default Hidden]
|
||||
;; ["@material-ui/core/Form" :default Form]
|
||||
["@material-ui/core/TextField" :default TextField]
|
||||
["@material-ui/core/TextareaAutosize" :default TextareaAutosize]
|
||||
["@material-ui/core/Card" :default Card]
|
||||
["@material-ui/core/CardActions" :default CardActions]
|
||||
["@material-ui/core/CardContent" :default CardContent]
|
||||
["@material-ui/core/CardHeader" :default CardHeader]
|
||||
["@material-ui/core/CardMedia" :default CardMedia]
|
||||
["@material-ui/core/Collapse" :default Collapse]
|
||||
["@material-ui/core/Avatar" :default Avatar]
|
||||
["@material-ui/core/CircularProgress" :default CircularProgress]
|
||||
["@material-ui/core/Badge" :default Badge]
|
||||
["@material-ui/core/Tooltip" :default Tooltip]
|
||||
["@material-ui/core/Dialog" :default Dialog]
|
||||
["@material-ui/core/DialogTitle" :default DialogTitle]
|
||||
["@material-ui/core/DialogContent" :default DialogContent]
|
||||
["@material-ui/core/DialogActions" :default DialogActions]
|
||||
["@material-ui/icons/Favorite" :default FavoriteIcon]
|
||||
["@material-ui/icons/Add" :default AddIcon]
|
||||
["@material-ui/icons/Share" :default ShareIcon]
|
||||
["@material-ui/icons/MoreVert" :default MoreVertIcon]
|
||||
))
|
||||
|
||||
(defn custom-theme []
|
||||
(createMuiTheme
|
||||
(clj->js
|
||||
{:palette
|
||||
{:type "light"
|
||||
;; :primary (.-purple colors)
|
||||
;; :secondary (.-green colors)
|
||||
}
|
||||
:typography
|
||||
{:useNextVariants true}})))
|
||||
|
||||
(defonce theme-provider (r/adapt-class MuiThemeProvider))
|
||||
(defonce css-baseline (r/adapt-class CssBaseline))
|
||||
(defonce app-bar (r/adapt-class AppBar))
|
||||
(defonce divider (r/adapt-class Divider))
|
||||
(defonce tool-bar (r/adapt-class ToolBar))
|
||||
(defonce button (r/adapt-class Button))
|
||||
(defonce icon-button (r/adapt-class IconButton))
|
||||
(defonce typography (r/adapt-class Typography))
|
||||
(defonce container (r/adapt-class Container))
|
||||
(defonce box (r/adapt-class Box))
|
||||
(defonce snackbar (r/adapt-class Snackbar))
|
||||
(defonce link (r/adapt-class Link))
|
||||
(defonce checkbox (r/adapt-class Checkbox))
|
||||
(defonce grid (r/adapt-class Grid))
|
||||
(defonce grid-list (r/adapt-class GridList))
|
||||
(defonce paper (r/adapt-class Paper))
|
||||
(defonce collapse (r/adapt-class Collapse))
|
||||
(defonce avatar (r/adapt-class Avatar))
|
||||
(defonce favorite-icon (r/adapt-class FavoriteIcon))
|
||||
(defonce add-icon (r/adapt-class AddIcon))
|
||||
(defonce fab (r/adapt-class Fab))
|
||||
(defonce share-icon (r/adapt-class ShareIcon))
|
||||
(defonce more-vert-icon (r/adapt-class MoreVertIcon))
|
||||
(defonce circular-progress (r/adapt-class CircularProgress))
|
||||
(defonce badge (r/adapt-class Badge))
|
||||
(defonce text-field (r/adapt-class TextField))
|
||||
(defonce textarea (r/adapt-class TextareaAutosize))
|
||||
(defonce tooltip (r/adapt-class Tooltip))
|
||||
(defonce dialog (r/adapt-class Dialog))
|
||||
(defonce dialog-title (r/adapt-class DialogTitle))
|
||||
(defonce dialog-content (r/adapt-class DialogContent))
|
||||
(defonce dialog-actions (r/adapt-class DialogActions))
|
||||
(defonce menu-icon (r/adapt-class MenuIcon))
|
||||
(defonce drawer (r/adapt-class SwipeableDrawer))
|
||||
(defonce chip (r/adapt-class Chip))
|
||||
(defonce list (r/adapt-class List))
|
||||
(defonce list-item (r/adapt-class ListItem))
|
||||
(defonce list-item-text (r/adapt-class ListItemText))
|
||||
|
||||
;; card
|
||||
(defonce card (r/adapt-class Card))
|
||||
(defonce card-actions (r/adapt-class CardActions))
|
||||
(defonce card-content (r/adapt-class CardContent))
|
||||
(defonce card-actions (r/adapt-class CardActions))
|
||||
(defonce card-header (r/adapt-class CardHeader))
|
||||
(defonce card-media (r/adapt-class CardMedia))
|
||||
12
frontend/src/frontend/page.cljs
Normal file
12
frontend/src/frontend/page.cljs
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns frontend.page
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.layout :as layout]
|
||||
[frontend.routes :as routes]
|
||||
[frontend.state :as state]))
|
||||
|
||||
(rum/defc current-page < rum/reactive
|
||||
[]
|
||||
(let [state (rum/react state/state)
|
||||
current-page (get state :current-page :home)]
|
||||
(when-let [view (get routes/routes current-page)]
|
||||
(layout/frame (view) (:width state) (:add-link-dialog? state)))))
|
||||
12
frontend/src/frontend/routes.cljs
Normal file
12
frontend/src/frontend/routes.cljs
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns frontend.routes
|
||||
(:require [frontend.components.home :as home]
|
||||
[frontend.components.link :as link]
|
||||
[frontend.components.settings :as settings]
|
||||
[frontend.components.file :as file]
|
||||
))
|
||||
|
||||
(def routes
|
||||
{:home home/home
|
||||
:links link/links
|
||||
:settings settings/settings
|
||||
:edit-file file/edit})
|
||||
59
frontend/src/frontend/rum.cljs
Normal file
59
frontend/src/frontend/rum.cljs
Normal file
@@ -0,0 +1,59 @@
|
||||
(ns frontend.rum
|
||||
(:require [clojure.string :as s]
|
||||
[clojure.set :as set]
|
||||
[clojure.walk :as w]))
|
||||
|
||||
;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs
|
||||
|
||||
(defn kebab-case->camel-case
|
||||
"Converts from kebab case to camel case, eg: on-click to onClick"
|
||||
[input]
|
||||
(let [words (s/split input #"-")
|
||||
capitalize (->> (rest words)
|
||||
(map #(apply str (s/upper-case (first %)) (rest %))))]
|
||||
(apply str (first words) capitalize)))
|
||||
|
||||
(defn map-keys->camel-case
|
||||
"Stringifys all the keys of a cljs hashmap and converts them
|
||||
from kebab case to camel case. If :html-props option is specified,
|
||||
then rename the html properties values to their dom equivalent
|
||||
before conversion"
|
||||
[data & {:keys [html-props]}]
|
||||
(let [convert-to-camel (fn [[key value]]
|
||||
[(kebab-case->camel-case (name key)) value])]
|
||||
(w/postwalk (fn [x]
|
||||
(if (map? x)
|
||||
(let [new-map (if html-props
|
||||
(set/rename-keys x {:class :className :for :htmlFor})
|
||||
x)]
|
||||
(into {} (map convert-to-camel new-map)))
|
||||
x))
|
||||
data)))
|
||||
|
||||
;; adapted from https://github.com/tonsky/rum/issues/20
|
||||
(defn adapt-class [react-class]
|
||||
(fn [& args]
|
||||
(let [[opts children] (if (map? (first args))
|
||||
[(first args) (rest args)]
|
||||
[{} args])
|
||||
type# (first children)
|
||||
;; we have to make sure to check if the children is sequential
|
||||
;; as a list can be returned, eg: from a (for)
|
||||
new-children (if (sequential? type#)
|
||||
(let [result (sablono.interpreter/interpret children)]
|
||||
(if (sequential? result)
|
||||
result
|
||||
[result]))
|
||||
children)
|
||||
;; convert any options key value to a react element, if
|
||||
;; a valid html element tag is used, using sablono
|
||||
vector->react-elems (fn [[key val]]
|
||||
(if (sequential? val)
|
||||
[key (sablono.interpreter/interpret val)]
|
||||
[key val]))
|
||||
new-options (into {} (map vector->react-elems opts))]
|
||||
;; (.dir js/console new-children)
|
||||
(apply js/React.createElement react-class
|
||||
;; sablono html-to-dom-attrs does not work for nested hashmaps
|
||||
(clj->js (map-keys->camel-case new-options :html-props true))
|
||||
new-children))))
|
||||
19
frontend/src/frontend/state.cljs
Normal file
19
frontend/src/frontend/state.cljs
Normal file
@@ -0,0 +1,19 @@
|
||||
(ns frontend.state
|
||||
(:require [frontend.storage :as storage]))
|
||||
|
||||
(def state (atom {:current-page :home
|
||||
:cloning? false
|
||||
:cloned? (storage/get :cloned?)
|
||||
:files []
|
||||
:contents {} ; file name -> string
|
||||
:current-file nil
|
||||
:loadings {} ; file name -> bool
|
||||
:github-username ""
|
||||
:github-token ""
|
||||
:github-repo ""
|
||||
:width nil
|
||||
:drawer? false
|
||||
:tasks {}
|
||||
:links []
|
||||
:add-link-dialog? false
|
||||
}))
|
||||
19
frontend/src/frontend/storage.cljs
Normal file
19
frontend/src/frontend/storage.cljs
Normal file
@@ -0,0 +1,19 @@
|
||||
(ns frontend.storage
|
||||
(:refer-clojure :exclude [get set remove])
|
||||
(:require [cljs.reader :as reader]))
|
||||
|
||||
(defn get
|
||||
[key]
|
||||
(reader/read-string ^js (.getItem js/localStorage (name key))))
|
||||
|
||||
(defn set
|
||||
[key value]
|
||||
(.setItem ^js js/localStorage (name key) (pr-str value)))
|
||||
|
||||
(defn remove
|
||||
[key]
|
||||
(.removeItem ^js js/localStorage (name key)))
|
||||
|
||||
(defn clear
|
||||
[]
|
||||
(.clear ^js js/localStorage))
|
||||
88
frontend/src/frontend/util.cljs
Normal file
88
frontend/src/frontend/util.cljs
Normal file
@@ -0,0 +1,88 @@
|
||||
(ns frontend.util
|
||||
(:require [goog.object :as gobj]
|
||||
[promesa.core :as p]
|
||||
[clojure.walk :as walk]))
|
||||
|
||||
(defn evalue
|
||||
[event]
|
||||
(gobj/getValueByKeys event "target" "value"))
|
||||
|
||||
(defn p-handle
|
||||
([p ok-handler]
|
||||
(p-handle p ok-handler (fn [error] (prn "p-handle error: " error))))
|
||||
([p ok-handler error-handler]
|
||||
(-> p
|
||||
(p/then (fn [result]
|
||||
(ok-handler result)))
|
||||
(p/catch (fn [error]
|
||||
(error-handler error))))))
|
||||
|
||||
(defn get-width
|
||||
[]
|
||||
(gobj/get js/window "innerWidth"))
|
||||
|
||||
(defn listen
|
||||
"Register an event `handler` for events of `type` on `target`."
|
||||
[event-handler target type handler & [opts]]
|
||||
(.listen event-handler target (name type) handler (clj->js opts)))
|
||||
|
||||
(defn indexed
|
||||
[coll]
|
||||
(map-indexed vector coll))
|
||||
|
||||
(defn find-first
|
||||
[pred coll]
|
||||
(first (filter pred coll)))
|
||||
|
||||
(defn get-local-date
|
||||
[]
|
||||
(let [date (js/Date.)
|
||||
year (.getFullYear date)
|
||||
month (inc (.getMonth date))
|
||||
day (.getDate date)
|
||||
hour (.getHours date)
|
||||
minute (.getMinutes date)]
|
||||
{:year year
|
||||
:month month
|
||||
:day day
|
||||
:hour hour
|
||||
:minute minute}))
|
||||
|
||||
(defn dissoc-in
|
||||
"Dissociates an entry from a nested associative structure returning a new
|
||||
nested structure. keys is a sequence of keys. Any empty maps that result
|
||||
will not be present in the new structure."
|
||||
[m [k & ks :as keys]]
|
||||
(if ks
|
||||
(if-let [nextmap (get m k)]
|
||||
(let [newmap (dissoc-in nextmap ks)]
|
||||
(if (seq newmap)
|
||||
(assoc m k newmap)
|
||||
(dissoc m k)))
|
||||
m)
|
||||
(dissoc m k)))
|
||||
|
||||
(defn format
|
||||
[fmt & args]
|
||||
(apply goog.string/format fmt args))
|
||||
|
||||
(defn raw-html
|
||||
[content]
|
||||
[:div {:dangerouslySetInnerHTML
|
||||
{:__html content}}])
|
||||
|
||||
(defn json->clj
|
||||
[json-string]
|
||||
(-> json-string
|
||||
(js/JSON.parse)
|
||||
(js->clj :keywordize-keys true)))
|
||||
|
||||
(defn remove-nils
|
||||
"remove pairs of key-value that has nil value from a (possibly nested) map. also transform map to nil if all of its value are nil"
|
||||
[nm]
|
||||
(walk/postwalk
|
||||
(fn [el]
|
||||
(if (map? el)
|
||||
(not-empty (into {} (remove (comp nil? second)) el))
|
||||
el))
|
||||
nm))
|
||||
Reference in New Issue
Block a user