Compare commits
64 Commits
Author | SHA1 | Date | |
---|---|---|---|
926d4f9837 | |||
128498eac7 | |||
32094221c2 | |||
4cca4ee3b7 | |||
da717013de | |||
7096e6abeb | |||
e379896512 | |||
0c5442f0cd | |||
6c2aba84ef | |||
3e686fa199 | |||
2a8a1d11b8 | |||
c3d066016b | |||
64bf39da29 | |||
c25e02dee1 | |||
5be05ceea6 | |||
e8a041024c | |||
36f385c7f3 | |||
ddb8bbec53 | |||
1e55039a67 | |||
2346a82a46 | |||
b63c6bd318 | |||
b72a79c380 | |||
5cd7a7eef0 | |||
f6dc41498b | |||
1c912a1600 | |||
eeef7c94cd | |||
3c3391b3a6 | |||
52460024b9 | |||
48f7c8d18e | |||
571e0b65b6 | |||
7dc2047e97 | |||
f769e710d8 | |||
d09f698b71 | |||
8666f663ba | |||
22ccea893c | |||
362c406471 | |||
2a87037f06 | |||
53d0dcfb15 | |||
c892b5449b | |||
7cd9dca958 | |||
0e8ddc22c5 | |||
3671ad6199 | |||
7189c955c3 | |||
f56ecc0ba3 | |||
fdfca3f7a5 | |||
c61b2c67b7 | |||
d0d958a638 | |||
a437b5966f | |||
e2378279d7 | |||
1b49b668b3 | |||
03021614b5 | |||
50af86798a | |||
be01723be2 | |||
0a27a4ee29 | |||
e2f8ac6b78 | |||
d5e334dc09 | |||
1d6ba5960c | |||
bc29ca6c20 | |||
bf9fd4880f | |||
957e433847 | |||
edd631f520 | |||
2e1545a9f5 | |||
3e296080f5 | |||
d2ae6024ce |
@ -17,7 +17,7 @@ steps:
|
||||
- .mix
|
||||
|
||||
- name: test
|
||||
image: elixir:1.14.4-alpine
|
||||
image: elixir:1.18.3-alpine
|
||||
environment:
|
||||
TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
|
||||
HOST: testing.example.tld
|
||||
@ -26,13 +26,12 @@ steps:
|
||||
MIX_ENV: test
|
||||
commands:
|
||||
- apk add --no-cache build-base npm git
|
||||
- mix local.rebar --force --if-missing
|
||||
- mix local.hex --force --if-missing
|
||||
- mix local.rebar --force
|
||||
- mix local.hex --force
|
||||
- mix deps.get
|
||||
- npm set cache .npm
|
||||
- npm --prefix ./assets ci --no-audit --prefer-offline
|
||||
- npm run --prefix ./assets deploy
|
||||
- mix do phx.digest, gettext.extract
|
||||
- mix do phx.digest, gettext.extract, assets.deploy
|
||||
- mix test.all
|
||||
|
||||
- name: build and publish stable
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -28,10 +28,11 @@ npm-debug.log
|
||||
# The directory NPM downloads your dependencies sources to.
|
||||
/assets/node_modules/
|
||||
|
||||
# Since we are building assets from assets/,
|
||||
# we ignore priv/static. You may want to comment
|
||||
# this depending on your deployment strategy.
|
||||
/priv/static/
|
||||
# Ignore assets that are produced by build tools.
|
||||
/priv/static/assets/
|
||||
|
||||
# Ignore digested assets cache.
|
||||
/priv/static/cache_manifest.json
|
||||
|
||||
.elixir_ls/
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
elixir 1.14.4-otp-25
|
||||
erlang 25.3
|
||||
nodejs 18.15.0
|
||||
elixir 1.18.3-otp-27
|
||||
erlang 27.3.1
|
||||
nodejs 23.10.0
|
||||
|
13
Dockerfile
13
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM elixir:1.14.4-alpine AS build
|
||||
FROM elixir:1.18.3-otp-27-alpine AS build
|
||||
|
||||
# install build dependencies
|
||||
RUN apk add --no-cache build-base npm git python3
|
||||
@ -7,8 +7,8 @@ RUN apk add --no-cache build-base npm git python3
|
||||
WORKDIR /app
|
||||
|
||||
# install hex + rebar
|
||||
RUN mix local.hex --force && \
|
||||
mix local.rebar --force
|
||||
RUN mix local.rebar --force && \
|
||||
mix local.hex --force
|
||||
|
||||
# set build ENV
|
||||
ENV MIX_ENV=prod
|
||||
@ -25,24 +25,25 @@ RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
|
||||
COPY lib lib
|
||||
COPY priv priv
|
||||
COPY assets assets
|
||||
RUN npm run --prefix ./assets deploy
|
||||
RUN mix do phx.digest, gettext.extract
|
||||
|
||||
# compile and build release
|
||||
# uncomment COPY if rel/ exists
|
||||
# COPY rel rel
|
||||
RUN mix do compile, release
|
||||
RUN mix do assets.deploy, compile, release
|
||||
|
||||
# prepare release image
|
||||
FROM alpine:latest AS app
|
||||
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache bash openssl libssl1.1 libcrypto1.1 libgcc libstdc++ ncurses-libs
|
||||
apk add --no-cache bash openssl libssl3 libcrypto3 libgcc libstdc++ ncurses-libs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN chown nobody:nobody /app
|
||||
|
||||
ENV MIX_ENV=prod
|
||||
|
||||
USER nobody:nobody
|
||||
|
||||
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/memex ./
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
|
||||
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "@fortawesome/fontawesome-free/scss/regular";
|
||||
@import "@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "@fortawesome/fontawesome-free/scss/brands";
|
||||
|
||||
@import "components";
|
||||
|
||||
/* fix firefox scrollbars */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: rgba(161, 161, 170, var(--tw-bg-opacity)) white;
|
||||
}
|
||||
|
||||
.fa-fade {
|
||||
animation: pulse 1s ease-in-out 0s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { scale: 0.95; opacity: 0.5; }
|
||||
100% { scale: 1.0; opacity: 1; }
|
||||
}
|
||||
|
||||
// disconnect toast
|
||||
.phx-connected > #disconnect {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.phx-error > #disconnect {
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: #f36c69;
|
||||
display: block;
|
||||
margin: -1rem 0 2rem;
|
||||
}
|
||||
|
||||
/* LiveView specific classes for your customization */
|
||||
.phx-no-feedback.invalid-feedback,
|
||||
.phx-no-feedback .invalid-feedback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phx-click-loading {
|
||||
opacity: 0.5;
|
||||
transition: opacity 1s ease-out;
|
||||
}
|
||||
|
||||
.phx-loading{
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.phx-modal {
|
||||
opacity: 1!important;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.phx-modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15vh auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.phx-modal-close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.phx-modal-close:hover,
|
||||
.phx-modal-close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fade-in-scale {
|
||||
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
|
||||
}
|
||||
|
||||
.fade-out-scale {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||
}
|
||||
.fade-out {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
||||
}
|
||||
|
||||
@keyframes fade-in-scale-keys{
|
||||
0% { scale: 0.95; opacity: 0; }
|
||||
100% { scale: 1.0; opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out-scale-keys{
|
||||
0% { scale: 1.0; opacity: 1; }
|
||||
100% { scale: 0.95; opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fade-in-keys{
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out-keys{
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
@layer components {
|
||||
.input {
|
||||
@apply rounded-lg px-4 py-2 border focus:outline-none;
|
||||
@apply shadow-sm focus:shadow-lg;
|
||||
}
|
||||
|
||||
.input-primary {
|
||||
@apply bg-primary-900;
|
||||
@apply border-primary-900 hover:border-primary-800 active:border-primary-700;
|
||||
@apply text-primary-400 placeholder-primary-600;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply bg-primary-900;
|
||||
-ms-transform: scale(1.5);
|
||||
-moz-transform: scale(1.5);
|
||||
-webkit-transform: scale(1.5);
|
||||
-o-transform: scale(1.5);
|
||||
transform: scale(1.5);
|
||||
padding: 10px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply leading-5 tracking-wide;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply focus:outline-none px-4 py-2 rounded-lg;
|
||||
@apply shadow-sm active:shadow-lg;
|
||||
@apply border;
|
||||
@apply transition-all duration-300 ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-primary-900 active:bg-primary-800;
|
||||
@apply border-primary-900 hover:border-primary-800 active:border-primary-700;
|
||||
@apply text-primary-400;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-primary-800 active:bg-primary-700;
|
||||
@apply border-primary-800 hover:border-primary-700 active:border-primary-600;
|
||||
@apply text-primary-400;
|
||||
}
|
||||
|
||||
.btn-alert {
|
||||
@apply bg-red-800 active:bg-red-900;
|
||||
@apply border-red-800 active:border-red-900;
|
||||
@apply text-primary-300;
|
||||
}
|
||||
|
||||
.hr {
|
||||
@apply mx-auto border border-primary-600 w-full max-w-3xl;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply hover:underline;
|
||||
@apply transition-colors duration-500 ease-in-out;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@apply bg-primary-900;
|
||||
@apply text-primary-400;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply text-primary-400;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #8a6d3b;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.alert p {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.alert:empty {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
226
assets/css/style.css
Normal file
226
assets/css/style.css
Normal file
@ -0,0 +1,226 @@
|
||||
@import "tailwindcss" source("../..");
|
||||
|
||||
@theme {
|
||||
--color-primary-50: oklch(0.985 0 0);
|
||||
--color-primary-100: oklch(0.967 0.001 286.375);
|
||||
--color-primary-200: oklch(0.92 0.004 286.32);
|
||||
--color-primary-300: oklch(0.871 0.006 286.286);
|
||||
--color-primary-400: oklch(0.705 0.015 286.067);
|
||||
--color-primary-500: oklch(0.552 0.016 285.938);
|
||||
--color-primary-600: oklch(0.442 0.017 285.786);
|
||||
--color-primary-700: oklch(0.37 0.013 285.805);
|
||||
--color-primary-800: oklch(0.274 0.006 286.033);
|
||||
--color-primary-900: oklch(0.21 0.006 285.885);
|
||||
--color-primary-950: oklch(0.141 0.005 285.823);
|
||||
--font-display: "Nunito Sans", sans-serif;
|
||||
}
|
||||
|
||||
@import "@fortawesome/fontawesome-free/css/fontawesome" source("../..");
|
||||
@import "@fortawesome/fontawesome-free/css/regular" source("../..");
|
||||
@import "@fortawesome/fontawesome-free/css/solid" source("../..");
|
||||
@import "@fortawesome/fontawesome-free/css/brands" source("../..");
|
||||
|
||||
/* fix firefox scrollbars */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: rgba(161, 161, 170, var(--tw-bg-opacity)) white;
|
||||
}
|
||||
|
||||
.fa-fade {
|
||||
animation: pulse 1s ease-in-out 0s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { scale: 0.95; opacity: 0.5; }
|
||||
100% { scale: 1.0; opacity: 1; }
|
||||
}
|
||||
|
||||
/* disconnect toast */
|
||||
.phx-connected > #disconnect {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.phx-error > #disconnect {
|
||||
opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: #f36c69;
|
||||
display: block;
|
||||
margin: -1rem 0 2rem;
|
||||
}
|
||||
|
||||
/* LiveView specific classes for your customization */
|
||||
.phx-no-feedback.invalid-feedback,
|
||||
.phx-no-feedback .invalid-feedback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phx-click-loading {
|
||||
opacity: 0.5;
|
||||
transition: opacity 1s ease-out;
|
||||
}
|
||||
|
||||
.phx-loading{
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.phx-modal {
|
||||
opacity: 1!important;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.phx-modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15vh auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.phx-modal-close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.phx-modal-close:hover,
|
||||
.phx-modal-close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fade-in-scale {
|
||||
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
|
||||
}
|
||||
|
||||
.fade-out-scale {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||
}
|
||||
.fade-out {
|
||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
||||
}
|
||||
|
||||
@keyframes fade-in-scale-keys{
|
||||
0% { scale: 0.95; opacity: 0; }
|
||||
100% { scale: 1.0; opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out-scale-keys{
|
||||
0% { scale: 1.0; opacity: 1; }
|
||||
100% { scale: 0.95; opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fade-in-keys{
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out-keys{
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* components */
|
||||
.input {
|
||||
@apply px-4 py-2 rounded-lg border focus:outline-hidden;
|
||||
@apply shadow-sm focus:shadow-lg;
|
||||
}
|
||||
|
||||
.input-primary {
|
||||
@apply bg-primary-900;
|
||||
@apply border-primary-900 hover:border-primary-800 active:border-primary-700;
|
||||
@apply text-primary-400 placeholder-primary-600;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply bg-primary-900;
|
||||
-ms-transform: scale(1.5);
|
||||
-moz-transform: scale(1.5);
|
||||
-webkit-transform: scale(1.5);
|
||||
-o-transform: scale(1.5);
|
||||
transform: scale(1.5);
|
||||
padding: 10px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply tracking-wide leading-5;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-lg focus:outline-hidden;
|
||||
@apply shadow-sm active:shadow-lg;
|
||||
@apply border;
|
||||
@apply transition-all duration-300 ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-primary-900 active:bg-primary-800;
|
||||
@apply border-primary-900 hover:border-primary-800 active:border-primary-700;
|
||||
@apply text-primary-400;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-primary-800 active:bg-primary-700;
|
||||
@apply border-primary-800 hover:border-primary-700 active:border-primary-600;
|
||||
@apply text-primary-400;
|
||||
}
|
||||
|
||||
.btn-alert {
|
||||
@apply bg-rose-800 active:bg-rose-900;
|
||||
@apply border-rose-800 active:border-rose-900;
|
||||
@apply text-primary-300;
|
||||
}
|
||||
|
||||
.hr {
|
||||
@apply mx-auto w-full max-w-3xl border border-primary-600;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply hover:underline;
|
||||
@apply transition-colors duration-500 ease-in-out;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@apply bg-primary-900;
|
||||
@apply text-primary-400;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply text-primary-400;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #8a6d3b;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.alert p {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.alert:empty {
|
||||
@apply hidden;
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
// We import the CSS which is extracted to its own file by esbuild.
|
||||
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
|
||||
import '../css/app.scss'
|
||||
import '@fontsource/nunito-sans'
|
||||
|
||||
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
||||
@ -26,15 +23,19 @@ import 'phoenix_html'
|
||||
import { Socket } from 'phoenix'
|
||||
import { LiveSocket } from 'phoenix_live_view'
|
||||
import topbar from 'topbar'
|
||||
|
||||
import CtrlEnter from './ctrlenter'
|
||||
import Date from './date'
|
||||
import DateTime from './datetime'
|
||||
import SanitizeTags from './sanitizetags'
|
||||
import SanitizeTitles from './sanitizetitles'
|
||||
|
||||
const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
|
||||
let csrfToken
|
||||
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
|
||||
const liveSocket = new LiveSocket('/live', Socket, {
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: { Date, DateTime }
|
||||
hooks: { CtrlEnter, Date, DateTime, SanitizeTags, SanitizeTitles }
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
|
11
assets/js/ctrlenter.js
Normal file
11
assets/js/ctrlenter.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
addFormSubmit (context) {
|
||||
context.el.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
context.el.dispatchEvent(
|
||||
new Event('submit', { bubbles: true, cancelable: true }))
|
||||
}
|
||||
})
|
||||
},
|
||||
mounted () { this.addFormSubmit(this) }
|
||||
}
|
11
assets/js/sanitizetags.js
Normal file
11
assets/js/sanitizetags.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
SanitizeTags (context) {
|
||||
context.el.addEventListener('keyup', (e) => {
|
||||
e.target.value = e.target.value
|
||||
.replace(' ', ',')
|
||||
.replace(',,', ',')
|
||||
.replace(/[^a-zA-Z0-9,]/, '')
|
||||
})
|
||||
},
|
||||
mounted () { this.SanitizeTags(this) }
|
||||
}
|
10
assets/js/sanitizetitles.js
Normal file
10
assets/js/sanitizetitles.js
Normal file
@ -0,0 +1,10 @@
|
||||
export default {
|
||||
SanitizeTitles (context) {
|
||||
context.el.addEventListener('keyup', (e) => {
|
||||
e.target.value = e.target.value
|
||||
.replace(' ', '-')
|
||||
.replace(/[^a-zA-Z0-9-]/, '')
|
||||
})
|
||||
},
|
||||
mounted () { this.SanitizeTitles(this) }
|
||||
}
|
27066
assets/package-lock.json
generated
27066
assets/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,47 +1,18 @@
|
||||
{
|
||||
"repository": {},
|
||||
"description": " ",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "v18.15.0",
|
||||
"npm": "9.5.0"
|
||||
"node": "v23.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "NODE_ENV=production webpack --mode production",
|
||||
"watch": "webpack --mode development --watch --watch-options-stdin",
|
||||
"format": "standard --fix",
|
||||
"test": "standard"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/nunito-sans": "^4.5.10",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"topbar": "^2.0.1"
|
||||
"@fontsource/nunito-sans": "^5.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"topbar": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.2",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"npm-check-updates": "^16.10.8",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-loader": "^7.2.4",
|
||||
"postcss-preset-env": "^8.3.1",
|
||||
"sass": "^1.62.0",
|
||||
"sass-loader": "^13.2.2",
|
||||
"standard": "^17.0.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.7",
|
||||
"webpack": "^5.79.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.2"
|
||||
"npm-check-updates": "^17.1.16",
|
||||
"standard": "^17.1.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
@ -1,46 +0,0 @@
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'../lib/**/*.{ex,heex,leex,eex}',
|
||||
'./js/**/*.js'
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
primary: colors.zinc,
|
||||
black: colors.black,
|
||||
white: colors.white,
|
||||
gray: colors.neutral,
|
||||
indigo: colors.indigo,
|
||||
red: colors.rose,
|
||||
yellow: colors.amber
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Nunito Sans', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont']
|
||||
},
|
||||
extend: {
|
||||
spacing: {
|
||||
128: '32rem',
|
||||
192: '48rem',
|
||||
256: '64rem'
|
||||
},
|
||||
minWidth: {
|
||||
4: '1rem',
|
||||
8: '2rem',
|
||||
12: '3rem',
|
||||
16: '4rem',
|
||||
20: '8rem'
|
||||
},
|
||||
maxWidth: {
|
||||
4: '1rem',
|
||||
8: '2rem',
|
||||
12: '3rem',
|
||||
16: '4rem',
|
||||
20: '8rem'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
const path = require('path')
|
||||
const glob = require('glob')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const TerserPlugin = require('terser-webpack-plugin')
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
|
||||
module.exports = (env, options) => {
|
||||
const devMode = options.mode !== 'production'
|
||||
|
||||
return {
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({ parallel: true, extractComments: true }),
|
||||
new CssMinimizerPlugin({})
|
||||
]
|
||||
},
|
||||
entry: {
|
||||
app: glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, '../priv/static/js'),
|
||||
publicPath: '/js/'
|
||||
},
|
||||
devtool: devMode ? 'eval-cheap-module-source-map' : undefined,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.s?css$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'postcss-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
type: 'asset/resource',
|
||||
generator: { filename: 'fonts/[name].[ext]' }
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
|
||||
new CopyWebpackPlugin({ patterns: [{ from: 'static/', to: '../' }] })
|
||||
]
|
||||
}
|
||||
}
|
53
changelog.md
53
changelog.md
@ -1,3 +1,56 @@
|
||||
# v0.1.20
|
||||
- Update deps
|
||||
- Improve accuracy of timestamps
|
||||
|
||||
# v0.1.19
|
||||
- Add backlinks
|
||||
- Fix visibility issues with multiple users
|
||||
|
||||
# v0.1.18
|
||||
- Update deps
|
||||
- Fix content not escaping HTML properly
|
||||
- Add placeholder for empty notes and contexts
|
||||
- Marks some required fields as required
|
||||
|
||||
# v0.1.17
|
||||
- Fix new invite button not working
|
||||
- Fix some descriptions possibly overflowing widths
|
||||
- Update dependencies
|
||||
|
||||
# v0.1.16
|
||||
- Fix empty invite index page
|
||||
- Fix faq copy
|
||||
- Fix issue with emails
|
||||
- Update deps
|
||||
|
||||
# v0.1.15
|
||||
- Sanitize titles while they are being typed
|
||||
- Sanitize tags while they are being typed
|
||||
- Remove requirement for note and content to have content
|
||||
- Prevent possible additional submissions
|
||||
- Fix content being displayed when blank
|
||||
|
||||
# v0.1.14
|
||||
- Fix issue with item content not able to be displayed sometimes
|
||||
|
||||
# v0.1.13
|
||||
- Update dependencies
|
||||
- Fix debounces
|
||||
- Allow space as delimiter for tags
|
||||
- Add bottom padding to page
|
||||
- Make pipeline step not require content
|
||||
- Make content previews resizable
|
||||
- Fix live flashes not dismissable by click
|
||||
- Fix disconnection modal not displaying
|
||||
- Submit items with ctrl-enter
|
||||
- Display backlinks in pipeline description
|
||||
- Modify backlink format
|
||||
|
||||
# v0.1.12
|
||||
- Code quality fixes
|
||||
- Fix error/404 pages not rendering properly
|
||||
- Update dependencies
|
||||
|
||||
# v0.1.11
|
||||
- Update dependencies
|
||||
- ee cummings even more
|
||||
|
@ -8,26 +8,31 @@
|
||||
import Config
|
||||
|
||||
config :memex,
|
||||
ecto_repos: [Memex.Repo],
|
||||
generators: [binary_id: true]
|
||||
env: :dev,
|
||||
ecto_repos: [Memex.Repo]
|
||||
|
||||
config :memex, Memex.Accounts, registration: System.get_env("REGISTRATION", "invite")
|
||||
|
||||
# Configures the endpoint
|
||||
config :memex, MemexWeb.Endpoint,
|
||||
adapter: Bandit.PhoenixAdapter,
|
||||
url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
|
||||
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
|
||||
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
|
||||
render_errors: [view: MemexWeb.ErrorView, accepts: ~w(html json), layout: false],
|
||||
render_errors: [
|
||||
formats: [html: MemexWeb.ErrorHTML, json: MemexWeb.ErrorJSON],
|
||||
layout: false
|
||||
],
|
||||
pubsub_server: Memex.PubSub,
|
||||
live_view: [signing_salt: "zOLgd3lr"]
|
||||
|
||||
config :memex, Memex.Application, automigrate: false
|
||||
|
||||
config :memex, :generators,
|
||||
migration: true,
|
||||
binary_id: true,
|
||||
sample_binary_id: "11111111-1111-1111-1111-111111111111"
|
||||
migration: true,
|
||||
sample_binary_id: "11111111-1111-1111-1111-111111111111",
|
||||
timestamp_type: :utc_datetime_usec
|
||||
|
||||
# Configures the mailer
|
||||
#
|
||||
@ -51,14 +56,25 @@ config :memex, Oban,
|
||||
queues: [default: 10, mailers: 20]
|
||||
|
||||
# Configure esbuild (the version is required)
|
||||
# config :esbuild,
|
||||
# version: "0.14.0",
|
||||
# default: [
|
||||
# args:
|
||||
# ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||
# cd: Path.expand("../assets", __DIR__),
|
||||
# env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
# ]
|
||||
config :esbuild,
|
||||
version: "0.17.11",
|
||||
memex: [
|
||||
args:
|
||||
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --loader:.woff=file --loader:.woff2=file),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
]
|
||||
|
||||
# Configure tailwind (the version is required)
|
||||
config :tailwind,
|
||||
version: "4.0.0",
|
||||
memex: [
|
||||
args: ~w(
|
||||
--input=css/style.css
|
||||
--output=../priv/static/assets/style.css
|
||||
),
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
|
@ -2,6 +2,7 @@ import Config
|
||||
|
||||
# Configure your database
|
||||
config :memex, Memex.Repo,
|
||||
stacktrace: true,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
||||
@ -12,21 +13,14 @@ config :memex, Memex.Repo,
|
||||
# watchers to your application. For example, we use it
|
||||
# with esbuild to bundle .js and .css sources.
|
||||
config :memex, MemexWeb.Endpoint,
|
||||
http: [ip: {0, 0, 0, 0}, port: 4000],
|
||||
check_origin: false,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
|
||||
watchers: [
|
||||
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
|
||||
# esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
|
||||
node: [
|
||||
"node_modules/webpack/bin/webpack.js",
|
||||
"--mode",
|
||||
"development",
|
||||
"--watch",
|
||||
"--watch-options-stdin",
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
esbuild: {Esbuild, :install_and_run, [:memex, ~w(--sourcemap=inline --watch)]},
|
||||
tailwind: {Tailwind, :install_and_run, [:memex, ~w(--watch)]}
|
||||
]
|
||||
|
||||
# ## SSL Support
|
||||
@ -57,7 +51,7 @@ config :memex, MemexWeb.Endpoint,
|
||||
config :memex, MemexWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"priv/gettext/.*(po)$",
|
||||
~r"lib/memex_web/*/.*(ex)$"
|
||||
]
|
||||
@ -73,3 +67,9 @@ config :phoenix, :stacktrace_depth, 20
|
||||
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
config :phoenix_live_view,
|
||||
# Include HEEx debug annotations as HTML comments in rendered markup
|
||||
debug_heex_annotations: true,
|
||||
# Enable helpful, but potentially expensive runtime checks
|
||||
enable_expensive_runtime_checks: true
|
||||
|
@ -14,6 +14,8 @@ config :memex, MemexWeb.Endpoint, cache_static_manifest: "priv/static/cache_mani
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
|
||||
config :memex, env: :prod
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# To get SSL working, you will need to add the `https` key
|
||||
|
@ -7,11 +7,21 @@ import Config
|
||||
# any compile-time configuration in here, as it won't be applied.
|
||||
# The block below contains prod specific runtime configuration.
|
||||
|
||||
# Start the phoenix server if environment is set and running in a release
|
||||
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
|
||||
# ## Using releases
|
||||
#
|
||||
# If you use `mix release`, you need to explicitly enable the server
|
||||
# by passing the PHX_SERVER=true when you start it:
|
||||
#
|
||||
# PHX_SERVER=true bin/memex start
|
||||
#
|
||||
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
|
||||
# script that automatically sets the env var above.
|
||||
if System.get_env("PHX_SERVER") do
|
||||
config :memex, MemexWeb.Endpoint, server: true
|
||||
end
|
||||
|
||||
config :memex, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||
|
||||
# Set default locale
|
||||
config :gettext, :default_locale, System.get_env("LOCALE", "en_US")
|
||||
|
||||
@ -66,7 +76,7 @@ if config_env() == :prod do
|
||||
System.get_env("SECRET_KEY_BASE") ||
|
||||
raise """
|
||||
environment variable SECRET_KEY_BASE is missing.
|
||||
You can generate one by running: mix phx.gen.secret
|
||||
You can generate one by running: priv/random.sh
|
||||
"""
|
||||
|
||||
config :memex, MemexWeb.Endpoint, secret_key_base: secret_key_base
|
||||
|
@ -9,8 +9,9 @@ config :bcrypt_elixir, :log_rounds, 1
|
||||
# to provide built-in test partitioning in CI environment.
|
||||
# Run `mix help test` for more information.
|
||||
config :memex, Memex.Repo,
|
||||
pool_size: 10,
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 10
|
||||
timeout: 60000
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
@ -19,6 +20,8 @@ config :memex, MemexWeb.Endpoint,
|
||||
secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
|
||||
server: false
|
||||
|
||||
config :memex, env: :test
|
||||
|
||||
# In test we don't send emails.
|
||||
config :memex, Memex.Mailer, adapter: Swoosh.Adapters.Test
|
||||
|
||||
@ -26,10 +29,10 @@ config :memex, Memex.Mailer, adapter: Swoosh.Adapters.Test
|
||||
config :memex, Memex.Accounts, registration: "public"
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
config :logger, level: :warning
|
||||
|
||||
# Initialize plugs at runtime for faster test compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
# Disable Oban
|
||||
config :memex, Oban, queues: false, plugins: false
|
||||
config :memex, Oban, queues: false, plugins: false, testing: :manual
|
||||
|
@ -113,7 +113,7 @@ In `test` mode (or in the Docker container), memEx will listen for the same envi
|
||||
In `prod` mode (or in the Docker container), memEx will listen for the same environment variables as dev mode, but also include the following at runtime:
|
||||
|
||||
- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated
|
||||
with `docker run -it shibaobun/memex mix phx.gen.secret` and set for server to start.
|
||||
with `docker run -it shibaobun/memex priv/random.sh` and set for server to start.
|
||||
- `SMTP_HOST`: The url for your SMTP email provider. Must be set
|
||||
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
|
||||
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
|
||||
|
BIN
home.png
BIN
home.png
Binary file not shown.
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 340 KiB |
30
lib/memex.ex
30
lib/memex.ex
@ -6,4 +6,34 @@ defmodule Memex do
|
||||
Contexts are also responsible for managing your data, regardless
|
||||
if it comes from the database, an external API or others.
|
||||
"""
|
||||
|
||||
def context do
|
||||
quote do
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
import Ecto.Query
|
||||
alias Ecto.{Changeset, Multi, Queryable, UUID}
|
||||
alias Memex.Accounts.User
|
||||
alias Memex.Repo
|
||||
end
|
||||
end
|
||||
|
||||
def schema do
|
||||
quote do
|
||||
use Ecto.Schema
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
import Ecto.{Changeset, Query}
|
||||
alias Ecto.{Association, Changeset, Queryable, UUID}
|
||||
alias Memex.Accounts.User
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used, dispatch to the appropriate context/schema/etc.
|
||||
"""
|
||||
defmacro __using__(which) when is_atom(which) do
|
||||
apply(__MODULE__, which, [])
|
||||
end
|
||||
end
|
||||
|
@ -3,10 +3,9 @@ defmodule Memex.Accounts do
|
||||
The Accounts context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.{Mailer, Repo}
|
||||
alias Memex.Accounts.{Invite, Invites, User, UserToken}
|
||||
alias Ecto.{Changeset, Multi}
|
||||
use Memex, :context
|
||||
alias Memex.Mailer
|
||||
alias Memex.Accounts.{Invite, Invites, UserToken}
|
||||
alias Oban.Job
|
||||
|
||||
## Database getters
|
||||
@ -117,7 +116,7 @@ defmodule Memex.Accounts do
|
||||
Multi.new()
|
||||
|> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
|
||||
|> Multi.run(:use_invite, fn _changes_so_far, _repo ->
|
||||
if allow_registration?() and invite_token |> is_nil() do
|
||||
if allow_registration?() and !invite_token do
|
||||
{:ok, nil}
|
||||
else
|
||||
Invites.use_invite(invite_token)
|
||||
@ -219,7 +218,7 @@ defmodule Memex.Accounts do
|
||||
|
||||
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
||||
%UserToken{sent_to: email} <- Repo.one(query),
|
||||
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
|
||||
{:ok, _result} <- Repo.transaction(user_email_multi(user, email, context)) do
|
||||
:ok
|
||||
else
|
||||
_error_tuple -> :error
|
||||
@ -405,15 +404,15 @@ defmodule Memex.Accounts do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_admin?(%User{role: :admin})
|
||||
iex> admin?(%User{role: :admin})
|
||||
true
|
||||
|
||||
iex> is_admin?(%User{})
|
||||
iex> admin?(%User{})
|
||||
false
|
||||
|
||||
"""
|
||||
@spec is_admin?(User.t()) :: boolean()
|
||||
def is_admin?(%User{id: user_id}) do
|
||||
@spec admin?(User.t()) :: boolean()
|
||||
def admin?(%User{id: user_id}) do
|
||||
Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin)
|
||||
end
|
||||
|
||||
@ -422,16 +421,16 @@ defmodule Memex.Accounts do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_already_admin?(%User{role: :admin})
|
||||
iex> already_admin?(%User{role: :admin})
|
||||
true
|
||||
|
||||
iex> is_already_admin?(%User{})
|
||||
iex> already_admin?(%User{})
|
||||
false
|
||||
|
||||
"""
|
||||
@spec is_already_admin?(User.t() | nil) :: boolean()
|
||||
def is_already_admin?(%User{role: :admin}), do: true
|
||||
def is_already_admin?(_invalid_user), do: false
|
||||
@spec already_admin?(User.t() | nil) :: boolean()
|
||||
def already_admin?(%User{role: :admin}), do: true
|
||||
def already_admin?(_invalid_user), do: false
|
||||
|
||||
## Confirmation
|
||||
|
||||
|
@ -5,24 +5,19 @@ defmodule Memex.Accounts.Invite do
|
||||
`:uses_left` is defined.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.{Association, Changeset, UUID}
|
||||
alias Memex.Accounts.User
|
||||
use Memex, :schema
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "invites" do
|
||||
field :name, :string
|
||||
field :token, :string
|
||||
field :uses_left, :integer, default: nil
|
||||
field :disabled_at, :naive_datetime
|
||||
field :disabled_at, :utc_datetime_usec
|
||||
|
||||
belongs_to :created_by, User
|
||||
|
||||
has_many :users, User
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
@ -30,12 +25,12 @@ defmodule Memex.Accounts.Invite do
|
||||
name: String.t(),
|
||||
token: token(),
|
||||
uses_left: integer() | nil,
|
||||
disabled_at: NaiveDateTime.t(),
|
||||
disabled_at: DateTime.t(),
|
||||
created_by: User.t() | nil | Association.NotLoaded.t(),
|
||||
created_by_id: User.id() | nil,
|
||||
users: [User.t()] | Association.NotLoaded.t(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
@type new_invite :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
|
@ -3,10 +3,8 @@ defmodule Memex.Accounts.Invites do
|
||||
The Invites context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Ecto.Multi
|
||||
alias Memex.Accounts.{Invite, User}
|
||||
alias Memex.Repo
|
||||
use Memex, :context
|
||||
alias Memex.Accounts.Invite
|
||||
|
||||
@invite_token_length 20
|
||||
|
||||
@ -125,7 +123,7 @@ defmodule Memex.Accounts.Invites do
|
||||
end
|
||||
|
||||
defp decrement_invite_changeset(%Invite{uses_left: 1} = invite) do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
now = DateTime.utc_now()
|
||||
invite |> Invite.update_changeset(%{uses_left: 0, disabled_at: now})
|
||||
end
|
||||
|
||||
|
@ -3,11 +3,8 @@ defmodule Memex.Accounts.User do
|
||||
A Memex user
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import MemexWeb.Gettext
|
||||
alias Ecto.{Association, Changeset, UUID}
|
||||
alias Memex.Accounts.{Invite, User}
|
||||
use Memex, :schema
|
||||
alias Memex.Accounts.Invite
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
@ -20,13 +17,12 @@ defmodule Memex.Accounts.User do
|
||||
:updated_at
|
||||
]}
|
||||
@derive {Inspect, except: [:password]}
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :password, :string, virtual: true
|
||||
field :hashed_password, :string
|
||||
field :confirmed_at, :naive_datetime
|
||||
field :current_password, :string, virtual: true, redact: true
|
||||
field :confirmed_at, :utc_datetime_usec
|
||||
field :role, Ecto.Enum, values: [:admin, :user], default: :user
|
||||
field :locale, :string
|
||||
|
||||
@ -34,7 +30,7 @@ defmodule Memex.Accounts.User do
|
||||
|
||||
belongs_to :invite, Invite
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@type t :: %User{
|
||||
@ -42,14 +38,14 @@ defmodule Memex.Accounts.User do
|
||||
email: String.t(),
|
||||
password: String.t(),
|
||||
hashed_password: String.t(),
|
||||
confirmed_at: NaiveDateTime.t(),
|
||||
confirmed_at: DateTime.t(),
|
||||
role: role(),
|
||||
locale: String.t() | nil,
|
||||
created_invites: [Invite.t()] | Association.NotLoaded.t(),
|
||||
invite: Invite.t() | nil | Association.NotLoaded.t(),
|
||||
invite_id: Invite.id() | nil,
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
@type new_user :: %User{}
|
||||
@type id :: UUID.t()
|
||||
@ -140,7 +136,7 @@ defmodule Memex.Accounts.User do
|
||||
|> cast(attrs, [:email])
|
||||
|> validate_email()
|
||||
|> case do
|
||||
%{changes: %{email: _}} = changeset -> changeset
|
||||
%{changes: %{email: _email}} = changeset -> changeset
|
||||
%{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change"))
|
||||
end
|
||||
end
|
||||
@ -171,7 +167,7 @@ defmodule Memex.Accounts.User do
|
||||
"""
|
||||
@spec confirm_changeset(t() | changeset()) :: changeset()
|
||||
def confirm_changeset(user_or_changeset) do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
now = DateTime.utc_now()
|
||||
user_or_changeset |> change(confirmed_at: now)
|
||||
end
|
||||
|
||||
@ -197,6 +193,8 @@ defmodule Memex.Accounts.User do
|
||||
"""
|
||||
@spec validate_current_password(changeset(), String.t()) :: changeset()
|
||||
def validate_current_password(changeset, password) do
|
||||
changeset = cast(changeset, %{current_password: password}, [:current_password])
|
||||
|
||||
if valid_password?(changeset.data, password),
|
||||
do: changeset,
|
||||
else: changeset |> add_error(:current_password, dgettext("errors", "is not valid"))
|
||||
|
@ -3,10 +3,7 @@ defmodule Memex.Accounts.UserToken do
|
||||
Schema for a user's session token
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
alias Ecto.{Association, UUID}
|
||||
alias Memex.Accounts.User
|
||||
use Memex, :schema
|
||||
|
||||
@hash_algorithm :sha256
|
||||
@rand_size 32
|
||||
@ -18,8 +15,6 @@ defmodule Memex.Accounts.UserToken do
|
||||
@change_email_validity_in_days 7
|
||||
@session_validity_in_days 60
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users_tokens" do
|
||||
field :token, :binary
|
||||
field :context, :string
|
||||
@ -37,7 +32,7 @@ defmodule Memex.Accounts.UserToken do
|
||||
sent_to: String.t(),
|
||||
user: User.t() | Association.NotLoaded.t(),
|
||||
user_id: User.id() | nil,
|
||||
inserted_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t()
|
||||
}
|
||||
@type new_user_token :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@ -155,7 +150,7 @@ defmodule Memex.Accounts.UserToken do
|
||||
from t in __MODULE__, where: t.user_id == ^user.id
|
||||
end
|
||||
|
||||
def user_and_contexts_query(user, [_ | _] = contexts) do
|
||||
def user_and_contexts_query(user, [_head | _rest] = contexts) do
|
||||
from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts
|
||||
end
|
||||
end
|
||||
|
@ -15,6 +15,7 @@ defmodule Memex.Application do
|
||||
MemexWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Memex.PubSub},
|
||||
{DNSCluster, query: Application.get_env(:memex, :dns_cluster_query) || :ignore},
|
||||
# Start the Endpoint (http/https)
|
||||
MemexWeb.Endpoint,
|
||||
# Add Oban
|
||||
|
@ -3,8 +3,8 @@ defmodule Memex.Contexts do
|
||||
The Contexts context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.{Accounts.User, Contexts.Context, Repo}
|
||||
use Memex, :context
|
||||
alias Memex.Contexts.Context
|
||||
|
||||
@doc """
|
||||
Returns the list of contexts.
|
||||
@ -22,16 +22,16 @@ defmodule Memex.Contexts do
|
||||
@spec list_contexts(search :: String.t() | nil, User.t()) :: [Context.t()]
|
||||
def list_contexts(search \\ nil, user)
|
||||
|
||||
def list_contexts(search, %{id: user_id}) when search |> is_nil() or search == "" do
|
||||
Repo.all(from c in Context, where: c.user_id == ^user_id, order_by: c.slug)
|
||||
def list_contexts(search, %{id: user_id}) when user_id |> is_binary() and search in [nil, ""] do
|
||||
Repo.all(from c in Context, order_by: c.slug)
|
||||
end
|
||||
|
||||
def list_contexts(search, %{id: user_id}) when search |> is_binary() do
|
||||
def list_contexts(search, %{id: user_id})
|
||||
when user_id |> is_binary() and search |> is_binary() do
|
||||
trimmed_search = String.trim(search)
|
||||
|
||||
Repo.all(
|
||||
from c in Context,
|
||||
where: c.user_id == ^user_id,
|
||||
where:
|
||||
fragment(
|
||||
"search @@ websearch_to_tsquery('english', ?)",
|
||||
@ -63,7 +63,7 @@ defmodule Memex.Contexts do
|
||||
@spec list_public_contexts(search :: String.t() | nil) :: [Context.t()]
|
||||
def list_public_contexts(search \\ nil)
|
||||
|
||||
def list_public_contexts(search) when search |> is_nil() or search == "" do
|
||||
def list_public_contexts(search) when search in [nil, ""] do
|
||||
Repo.all(from c in Context, where: c.visibility == :public, order_by: c.slug)
|
||||
end
|
||||
|
||||
@ -88,6 +88,42 @@ defmodule Memex.Contexts do
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of contexts that link to a particular slug.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> backlink(%User{id: 123})
|
||||
[%Context{}, ...]
|
||||
|
||||
iex> backlink("[other-context]", %User{id: 123})
|
||||
[%Context{content: "[other-context]"}, ...]
|
||||
|
||||
"""
|
||||
@spec backlink(String.t(), User.t()) :: [Context.t()]
|
||||
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
|
||||
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||
|
||||
Repo.all(
|
||||
from c in Context,
|
||||
where: fragment("? ~ ?", c.content, ^link_regex),
|
||||
order_by: c.slug
|
||||
)
|
||||
end
|
||||
|
||||
def backlink(link, _invalid_user) do
|
||||
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||
|
||||
Repo.all(
|
||||
from c in Context,
|
||||
where: fragment("? ~ ?", c.content, ^link_regex),
|
||||
where: c.visibility == :public,
|
||||
order_by: c.slug
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single context.
|
||||
|
||||
@ -103,12 +139,8 @@ defmodule Memex.Contexts do
|
||||
|
||||
"""
|
||||
@spec get_context!(Context.id(), User.t()) :: Context.t()
|
||||
def get_context!(id, %{id: user_id}) do
|
||||
Repo.one!(
|
||||
from c in Context,
|
||||
where: c.id == ^id,
|
||||
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
|
||||
)
|
||||
def get_context!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one!(from c in Context, where: c.id == ^id)
|
||||
end
|
||||
|
||||
def get_context!(id, _invalid_user) do
|
||||
@ -134,12 +166,8 @@ defmodule Memex.Contexts do
|
||||
|
||||
"""
|
||||
@spec get_context_by_slug(Context.slug(), User.t()) :: Context.t() | nil
|
||||
def get_context_by_slug(slug, %{id: user_id}) do
|
||||
Repo.one(
|
||||
from c in Context,
|
||||
where: c.slug == ^slug,
|
||||
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
|
||||
)
|
||||
def get_context_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one(from c in Context, where: c.slug == ^slug)
|
||||
end
|
||||
|
||||
def get_context_by_slug(slug, _invalid_user) do
|
||||
@ -194,23 +222,16 @@ defmodule Memex.Contexts do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_context(%Context{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> delete_context(%Context{user_id: 123}, %User{role: :admin})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> delete_context(%Context{}, %User{id: 123})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> delete_context(%Context{}, nil)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_context(Context.t(), User.t()) ::
|
||||
{:ok, Context.t()} | {:error, Context.changeset()}
|
||||
def delete_context(%Context{user_id: user_id} = context, %{id: user_id}) do
|
||||
context |> Repo.delete()
|
||||
end
|
||||
|
||||
def delete_context(%Context{} = context, %{role: :admin}) do
|
||||
def delete_context(%Context{} = context, %{id: user_id}) when user_id |> is_binary() do
|
||||
context |> Repo.delete()
|
||||
end
|
||||
|
||||
@ -228,13 +249,4 @@ defmodule Memex.Contexts do
|
||||
def change_context(%Context{} = context, attrs \\ %{}, user) do
|
||||
context |> Context.update_changeset(attrs, user)
|
||||
end
|
||||
|
||||
@spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
|
||||
def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
|
||||
def is_owner_or_admin?(_context, %{role: :admin}), do: true
|
||||
def is_owner_or_admin?(_context, _other_user), do: false
|
||||
|
||||
@spec is_owner?(Context.t(), User.t()) :: boolean()
|
||||
def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
|
||||
def is_owner?(_context, _other_user), do: false
|
||||
end
|
||||
|
@ -3,11 +3,8 @@ defmodule Memex.Contexts.Context do
|
||||
Represents a document that synthesizes multiple concepts as defined by notes
|
||||
into a single consideration
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import MemexWeb.Gettext
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Repo}
|
||||
|
||||
use Memex, :schema
|
||||
|
||||
@derive {Phoenix.Param, key: :slug}
|
||||
@derive {Jason.Encoder,
|
||||
@ -19,8 +16,6 @@ defmodule Memex.Contexts.Context do
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]}
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "contexts" do
|
||||
field :slug, :string
|
||||
field :content, :string
|
||||
@ -30,7 +25,7 @@ defmodule Memex.Contexts.Context do
|
||||
|
||||
field :user_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
@ -40,8 +35,8 @@ defmodule Memex.Contexts.Context do
|
||||
tags_string: String.t() | nil,
|
||||
visibility: :public | :private | :unlisted,
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
@type id :: UUID.t()
|
||||
@type slug :: String.t()
|
||||
@ -57,33 +52,34 @@ defmodule Memex.Contexts.Context do
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :content, :user_id, :visibility])
|
||||
|> validate_required([:slug, :user_id, :visibility])
|
||||
|> unique_constraint(:slug)
|
||||
|> unsafe_validate_unique(:slug, Repo)
|
||||
|> unsafe_validate_unique(:slug, Memex.Repo)
|
||||
end
|
||||
|
||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
|
||||
note
|
||||
def update_changeset(%__MODULE__{} = context, attrs, %User{id: user_id})
|
||||
when user_id |> is_binary() do
|
||||
context
|
||||
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
||||
|> cast_tags_string(attrs)
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :content, :visibility])
|
||||
|> validate_required([:slug, :visibility])
|
||||
|> unique_constraint(:slug)
|
||||
|> unsafe_validate_unique(:slug, Repo)
|
||||
|> unsafe_validate_unique(:slug, Memex.Repo)
|
||||
end
|
||||
|
||||
defp cast_tags_string(changeset, attrs) do
|
||||
changeset
|
||||
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
||||
|> cast(attrs, [:tags_string])
|
||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\,]+$/,
|
||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\, ]+$/,
|
||||
message:
|
||||
dgettext(
|
||||
"errors",
|
||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma-delimited"
|
||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma or space delimited"
|
||||
)
|
||||
)
|
||||
|> cast_tags()
|
||||
@ -98,9 +94,9 @@ defmodule Memex.Contexts.Context do
|
||||
|
||||
defp process_tags(tags_string) when tags_string |> is_binary() do
|
||||
tags_string
|
||||
|> String.split(",", trim: true)
|
||||
|> String.split([",", " "], trim: true)
|
||||
|> Enum.map(fn str -> str |> String.trim() end)
|
||||
|> Enum.reject(fn str -> str |> is_nil() end)
|
||||
|> Enum.reject(fn str -> str in [nil, ""] end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
|
@ -7,8 +7,8 @@ defmodule Memex.Email do
|
||||
`lib/memex_web/components/layouts/email_text.txt.eex` for text emails.
|
||||
"""
|
||||
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
import Swoosh.Email
|
||||
import MemexWeb.Gettext
|
||||
import Phoenix.Template
|
||||
alias Memex.Accounts.User
|
||||
alias MemexWeb.{EmailHTML, Layouts}
|
@ -3,8 +3,8 @@ defmodule Memex.Notes do
|
||||
The Notes context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.{Accounts.User, Notes.Note, Repo}
|
||||
use Memex, :context
|
||||
alias Memex.Notes.Note
|
||||
|
||||
@doc """
|
||||
Returns the list of notes.
|
||||
@ -22,16 +22,15 @@ defmodule Memex.Notes do
|
||||
@spec list_notes(search :: String.t() | nil, User.t()) :: [Note.t()]
|
||||
def list_notes(search \\ nil, user)
|
||||
|
||||
def list_notes(search, %{id: user_id}) when search |> is_nil() or search == "" do
|
||||
Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.slug)
|
||||
def list_notes(search, %{id: user_id}) when user_id |> is_binary() and search in [nil, ""] do
|
||||
Repo.all(from n in Note, order_by: n.slug)
|
||||
end
|
||||
|
||||
def list_notes(search, %{id: user_id}) when search |> is_binary() do
|
||||
def list_notes(search, %{id: user_id}) when user_id |> is_binary() and search |> is_binary() do
|
||||
trimmed_search = String.trim(search)
|
||||
|
||||
Repo.all(
|
||||
from n in Note,
|
||||
where: n.user_id == ^user_id,
|
||||
where:
|
||||
fragment(
|
||||
"search @@ websearch_to_tsquery('english', ?)",
|
||||
@ -62,7 +61,7 @@ defmodule Memex.Notes do
|
||||
@spec list_public_notes(search :: String.t() | nil) :: [Note.t()]
|
||||
def list_public_notes(search \\ nil)
|
||||
|
||||
def list_public_notes(search) when search |> is_nil() or search == "" do
|
||||
def list_public_notes(search) when search in [nil, ""] do
|
||||
Repo.all(from n in Note, where: n.visibility == :public, order_by: n.slug)
|
||||
end
|
||||
|
||||
@ -87,6 +86,42 @@ defmodule Memex.Notes do
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of notes that link to a particular slug.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> backlink(%User{id: 123})
|
||||
[%Note{}, ...]
|
||||
|
||||
iex> backlink("[other-note]", %User{id: 123})
|
||||
[%Note{content: "[other-note]"}, ...]
|
||||
|
||||
"""
|
||||
@spec backlink(String.t(), User.t()) :: [Note.t()]
|
||||
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
|
||||
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||
|
||||
Repo.all(
|
||||
from n in Note,
|
||||
where: fragment("? ~ ?", n.content, ^link_regex),
|
||||
order_by: n.slug
|
||||
)
|
||||
end
|
||||
|
||||
def backlink(link, _invalid_user) do
|
||||
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||
|
||||
Repo.all(
|
||||
from n in Note,
|
||||
where: fragment("? ~ ?", n.content, ^link_regex),
|
||||
where: n.visibility == :public,
|
||||
order_by: n.slug
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single note.
|
||||
|
||||
@ -102,12 +137,8 @@ defmodule Memex.Notes do
|
||||
|
||||
"""
|
||||
@spec get_note!(Note.id(), User.t()) :: Note.t()
|
||||
def get_note!(id, %{id: user_id}) do
|
||||
Repo.one!(
|
||||
from n in Note,
|
||||
where: n.id == ^id,
|
||||
where: n.user_id == ^user_id or n.visibility in [:public, :unlisted]
|
||||
)
|
||||
def get_note!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one!(from n in Note, where: n.id == ^id)
|
||||
end
|
||||
|
||||
def get_note!(id, _invalid_user) do
|
||||
@ -133,12 +164,8 @@ defmodule Memex.Notes do
|
||||
|
||||
"""
|
||||
@spec get_note_by_slug(Note.slug(), User.t()) :: Note.t() | nil
|
||||
def get_note_by_slug(slug, %{id: user_id}) do
|
||||
Repo.one(
|
||||
from n in Note,
|
||||
where: n.slug == ^slug,
|
||||
where: n.user_id == ^user_id or n.visibility in [:public, :unlisted]
|
||||
)
|
||||
def get_note_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one(from n in Note, where: n.slug == ^slug)
|
||||
end
|
||||
|
||||
def get_note_by_slug(slug, _invalid_user) do
|
||||
@ -192,22 +219,15 @@ defmodule Memex.Notes do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_note(%Note{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Note{}}
|
||||
|
||||
iex> delete_note(%Note{}, %User{role: :admin})
|
||||
{:ok, %Note{}}
|
||||
|
||||
iex> delete_note(%Note{}, %User{id: 123})
|
||||
{:ok, %Note{}}
|
||||
|
||||
iex> delete_note(%Note{}, nil)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
|
||||
def delete_note(%Note{user_id: user_id} = note, %{id: user_id}) do
|
||||
note |> Repo.delete()
|
||||
end
|
||||
|
||||
def delete_note(%Note{} = note, %{role: :admin}) do
|
||||
def delete_note(%Note{} = note, %{id: user_id}) when user_id |> is_binary() do
|
||||
note |> Repo.delete()
|
||||
end
|
||||
|
||||
@ -228,13 +248,4 @@ defmodule Memex.Notes do
|
||||
def change_note(%Note{} = note, attrs \\ %{}, user) do
|
||||
note |> Note.update_changeset(attrs, user)
|
||||
end
|
||||
|
||||
@spec is_owner_or_admin?(Note.t(), User.t()) :: boolean()
|
||||
def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
|
||||
def is_owner_or_admin?(_context, %{role: :admin}), do: true
|
||||
def is_owner_or_admin?(_context, _other_user), do: false
|
||||
|
||||
@spec is_owner?(Note.t(), User.t()) :: boolean()
|
||||
def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
|
||||
def is_owner?(_context, _other_user), do: false
|
||||
end
|
||||
|
@ -2,11 +2,8 @@ defmodule Memex.Notes.Note do
|
||||
@moduledoc """
|
||||
Schema for a user-written note
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import MemexWeb.Gettext
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Repo}
|
||||
|
||||
use Memex, :schema
|
||||
|
||||
@derive {Phoenix.Param, key: :slug}
|
||||
@derive {Jason.Encoder,
|
||||
@ -18,8 +15,6 @@ defmodule Memex.Notes.Note do
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]}
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "notes" do
|
||||
field :slug, :string
|
||||
field :content, :string
|
||||
@ -29,7 +24,7 @@ defmodule Memex.Notes.Note do
|
||||
|
||||
field :user_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
@ -39,8 +34,8 @@ defmodule Memex.Notes.Note do
|
||||
tags_string: String.t() | nil,
|
||||
visibility: :public | :private | :unlisted,
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
@type id :: UUID.t()
|
||||
@type slug :: String.t()
|
||||
@ -56,33 +51,34 @@ defmodule Memex.Notes.Note do
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :content, :user_id, :visibility])
|
||||
|> validate_required([:slug, :user_id, :visibility])
|
||||
|> unique_constraint(:slug)
|
||||
|> unsafe_validate_unique(:slug, Repo)
|
||||
|> unsafe_validate_unique(:slug, Memex.Repo)
|
||||
end
|
||||
|
||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
|
||||
def update_changeset(%__MODULE__{} = note, attrs, %User{id: user_id})
|
||||
when user_id |> is_binary() do
|
||||
note
|
||||
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
||||
|> cast_tags_string(attrs)
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :content, :visibility])
|
||||
|> validate_required([:slug, :visibility])
|
||||
|> unique_constraint(:slug)
|
||||
|> unsafe_validate_unique(:slug, Repo)
|
||||
|> unsafe_validate_unique(:slug, Memex.Repo)
|
||||
end
|
||||
|
||||
defp cast_tags_string(changeset, attrs) do
|
||||
changeset
|
||||
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
||||
|> cast(attrs, [:tags_string])
|
||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\,]+$/,
|
||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\, ]+$/,
|
||||
message:
|
||||
dgettext(
|
||||
"errors",
|
||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma-delimited"
|
||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma or space delimited"
|
||||
)
|
||||
)
|
||||
|> cast_tags()
|
||||
@ -97,9 +93,9 @@ defmodule Memex.Notes.Note do
|
||||
|
||||
defp process_tags(tags_string) when tags_string |> is_binary() do
|
||||
tags_string
|
||||
|> String.split(",", trim: true)
|
||||
|> String.split([",", " "], trim: true)
|
||||
|> Enum.map(fn str -> str |> String.trim() end)
|
||||
|> Enum.reject(fn str -> str |> is_nil() end)
|
||||
|> Enum.reject(fn str -> str in [nil, ""] end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
|
@ -3,8 +3,8 @@ defmodule Memex.Pipelines do
|
||||
The Pipelines context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.{Accounts.User, Pipelines.Pipeline, Repo}
|
||||
use Memex, :context
|
||||
alias Memex.Pipelines.Pipeline
|
||||
|
||||
@doc """
|
||||
Returns the list of pipelines.
|
||||
@ -22,16 +22,17 @@ defmodule Memex.Pipelines do
|
||||
@spec list_pipelines(search :: String.t() | nil, User.t()) :: [Pipeline.t()]
|
||||
def list_pipelines(search \\ nil, user)
|
||||
|
||||
def list_pipelines(search, %{id: user_id}) when search |> is_nil() or search == "" do
|
||||
Repo.all(from p in Pipeline, where: p.user_id == ^user_id, order_by: p.slug)
|
||||
def list_pipelines(search, %{id: user_id})
|
||||
when user_id |> is_binary() and search in [nil, ""] do
|
||||
Repo.all(from p in Pipeline, order_by: p.slug)
|
||||
end
|
||||
|
||||
def list_pipelines(search, %{id: user_id}) when search |> is_binary() do
|
||||
def list_pipelines(search, %{id: user_id})
|
||||
when user_id |> is_binary() and search |> is_binary() do
|
||||
trimmed_search = String.trim(search)
|
||||
|
||||
Repo.all(
|
||||
from p in Pipeline,
|
||||
where: p.user_id == ^user_id,
|
||||
where:
|
||||
fragment(
|
||||
"search @@ websearch_to_tsquery('english', ?)",
|
||||
@ -62,7 +63,7 @@ defmodule Memex.Pipelines do
|
||||
@spec list_public_pipelines(search :: String.t() | nil) :: [Pipeline.t()]
|
||||
def list_public_pipelines(search \\ nil)
|
||||
|
||||
def list_public_pipelines(search) when search |> is_nil() or search == "" do
|
||||
def list_public_pipelines(search) when search in [nil, ""] do
|
||||
Repo.all(from p in Pipeline, where: p.visibility == :public, order_by: p.slug)
|
||||
end
|
||||
|
||||
@ -102,12 +103,8 @@ defmodule Memex.Pipelines do
|
||||
|
||||
"""
|
||||
@spec get_pipeline!(Pipeline.id(), User.t()) :: Pipeline.t()
|
||||
def get_pipeline!(id, %{id: user_id}) do
|
||||
Repo.one!(
|
||||
from p in Pipeline,
|
||||
where: p.id == ^id,
|
||||
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
|
||||
)
|
||||
def get_pipeline!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one!(from p in Pipeline, where: p.id == ^id)
|
||||
end
|
||||
|
||||
def get_pipeline!(id, _invalid_user) do
|
||||
@ -133,12 +130,8 @@ defmodule Memex.Pipelines do
|
||||
|
||||
"""
|
||||
@spec get_pipeline_by_slug(Pipeline.slug(), User.t()) :: Pipeline.t() | nil
|
||||
def get_pipeline_by_slug(slug, %{id: user_id}) do
|
||||
Repo.one(
|
||||
from p in Pipeline,
|
||||
where: p.slug == ^slug,
|
||||
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
|
||||
)
|
||||
def get_pipeline_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one(from p in Pipeline, where: p.slug == ^slug)
|
||||
end
|
||||
|
||||
def get_pipeline_by_slug(slug, _invalid_user) do
|
||||
@ -149,6 +142,50 @@ defmodule Memex.Pipelines do
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of pipelines that link to a particular slug.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> backlink(%User{id: 123})
|
||||
[%Pipeline{}, ...]
|
||||
|
||||
iex> backlink("[other-pipeline]", %User{id: 123})
|
||||
[%Pipeline{description: "[other-pipeline]"}, ...]
|
||||
|
||||
"""
|
||||
@spec backlink(String.t(), User.t()) :: [Pipeline.t()]
|
||||
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
|
||||
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||
|
||||
Repo.all(
|
||||
from p in Pipeline,
|
||||
left_join: s in assoc(p, :steps),
|
||||
where:
|
||||
fragment("? ~ ?", p.description, ^link_regex) or
|
||||
fragment("? ~ ?", s.content, ^link_regex),
|
||||
distinct: true,
|
||||
order_by: p.slug
|
||||
)
|
||||
end
|
||||
|
||||
def backlink(link, _invalid_user) do
|
||||
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||
|
||||
Repo.all(
|
||||
from p in Pipeline,
|
||||
left_join: s in assoc(p, :steps),
|
||||
where:
|
||||
fragment("? ~ ?", p.description, ^link_regex) or
|
||||
fragment("? ~ ?", s.content, ^link_regex),
|
||||
where: p.visibility == :public,
|
||||
distinct: true,
|
||||
order_by: p.slug
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a pipeline.
|
||||
|
||||
@ -193,23 +230,16 @@ defmodule Memex.Pipelines do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_pipeline(%Pipeline{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> delete_pipeline(%Pipeline{}, %User{role: :admin})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> delete_pipeline(%Pipeline{}, %User{id: 123})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> delete_pipeline(%Pipeline{}, nil)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_pipeline(Pipeline.t(), User.t()) ::
|
||||
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
|
||||
def delete_pipeline(%Pipeline{user_id: user_id} = pipeline, %{id: user_id}) do
|
||||
pipeline |> Repo.delete()
|
||||
end
|
||||
|
||||
def delete_pipeline(%Pipeline{} = pipeline, %{role: :admin}) do
|
||||
def delete_pipeline(%Pipeline{} = pipeline, %{id: user_id}) when user_id |> is_binary() do
|
||||
pipeline |> Repo.delete()
|
||||
end
|
||||
|
||||
@ -230,13 +260,4 @@ defmodule Memex.Pipelines do
|
||||
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}, user) do
|
||||
pipeline |> Pipeline.update_changeset(attrs, user)
|
||||
end
|
||||
|
||||
@spec is_owner_or_admin?(Pipeline.t(), User.t()) :: boolean()
|
||||
def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
|
||||
def is_owner_or_admin?(_context, %{role: :admin}), do: true
|
||||
def is_owner_or_admin?(_context, _other_user), do: false
|
||||
|
||||
@spec is_owner?(Pipeline.t(), User.t()) :: boolean()
|
||||
def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
|
||||
def is_owner?(_context, _other_user), do: false
|
||||
end
|
||||
|
@ -2,11 +2,9 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
@moduledoc """
|
||||
Represents a chain of considerations to take to accomplish a task
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import MemexWeb.Gettext
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo}
|
||||
|
||||
use Memex, :schema
|
||||
alias Memex.Pipelines.Steps.Step
|
||||
|
||||
@derive {Phoenix.Param, key: :slug}
|
||||
@derive {Jason.Encoder,
|
||||
@ -19,8 +17,6 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
:steps,
|
||||
:updated_at
|
||||
]}
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "pipelines" do
|
||||
field :slug, :string
|
||||
field :description, :string
|
||||
@ -32,7 +28,7 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
|
||||
has_many :steps, Step, preload_order: [asc: :position]
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
@ -42,8 +38,8 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
tags_string: String.t() | nil,
|
||||
visibility: :public | :private | :unlisted,
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
@type id :: UUID.t()
|
||||
@type slug :: String.t()
|
||||
@ -61,11 +57,12 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
)
|
||||
|> validate_required([:slug, :user_id, :visibility])
|
||||
|> unique_constraint(:slug)
|
||||
|> unsafe_validate_unique(:slug, Repo)
|
||||
|> unsafe_validate_unique(:slug, Memex.Repo)
|
||||
end
|
||||
|
||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||
def update_changeset(%{user_id: user_id} = pipeline, attrs, %User{id: user_id}) do
|
||||
def update_changeset(%__MODULE__{} = pipeline, attrs, %User{id: user_id})
|
||||
when user_id |> is_binary() do
|
||||
pipeline
|
||||
|> cast(attrs, [:slug, :description, :tags, :visibility])
|
||||
|> cast_tags_string(attrs)
|
||||
@ -74,18 +71,18 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
)
|
||||
|> validate_required([:slug, :visibility])
|
||||
|> unique_constraint(:slug)
|
||||
|> unsafe_validate_unique(:slug, Repo)
|
||||
|> unsafe_validate_unique(:slug, Memex.Repo)
|
||||
end
|
||||
|
||||
defp cast_tags_string(changeset, attrs) do
|
||||
changeset
|
||||
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
||||
|> cast(attrs, [:tags_string])
|
||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\,]+$/,
|
||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\, ]+$/,
|
||||
message:
|
||||
dgettext(
|
||||
"errors",
|
||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma-delimited"
|
||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma or space delimited"
|
||||
)
|
||||
)
|
||||
|> cast_tags()
|
||||
@ -100,9 +97,9 @@ defmodule Memex.Pipelines.Pipeline do
|
||||
|
||||
defp process_tags(tags_string) when tags_string |> is_binary() do
|
||||
tags_string
|
||||
|> String.split(",", trim: true)
|
||||
|> String.split([",", " "], trim: true)
|
||||
|> Enum.map(fn str -> str |> String.trim() end)
|
||||
|> Enum.reject(fn str -> str |> is_nil() end)
|
||||
|> Enum.reject(fn str -> str in [nil, ""] end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
|
@ -2,10 +2,9 @@ defmodule Memex.Pipelines.Steps.Step do
|
||||
@moduledoc """
|
||||
Represents a step taken while executing a pipeline
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Pipelines.Pipeline}
|
||||
|
||||
use Memex, :schema
|
||||
alias Memex.Pipelines.Pipeline
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
@ -15,8 +14,6 @@ defmodule Memex.Pipelines.Steps.Step do
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]}
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "steps" do
|
||||
field :title, :string
|
||||
field :content, :string
|
||||
@ -25,7 +22,7 @@ defmodule Memex.Pipelines.Steps.Step do
|
||||
belongs_to :pipeline, Pipeline
|
||||
field :user_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
@ -35,8 +32,8 @@ defmodule Memex.Pipelines.Steps.Step do
|
||||
pipeline: Pipeline.t() | Ecto.Association.NotLoaded.t(),
|
||||
pipeline_id: Pipeline.id(),
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t())
|
||||
@ -44,35 +41,32 @@ defmodule Memex.Pipelines.Steps.Step do
|
||||
@doc false
|
||||
@spec create_changeset(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
|
||||
changeset()
|
||||
def create_changeset(attrs, position, %Pipeline{id: pipeline_id, user_id: user_id}, %User{
|
||||
id: user_id
|
||||
}) do
|
||||
def create_changeset(
|
||||
attrs,
|
||||
position,
|
||||
%Pipeline{id: pipeline_id, user_id: user_id},
|
||||
%User{id: user_id}
|
||||
) do
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, [:title, :content])
|
||||
|> change(pipeline_id: pipeline_id, user_id: user_id, position: position)
|
||||
|> validate_required([:title, :content, :user_id, :position])
|
||||
|> validate_required([:title, :user_id, :position])
|
||||
end
|
||||
|
||||
@spec update_changeset(t(), attrs :: map(), User.t()) ::
|
||||
changeset()
|
||||
def update_changeset(
|
||||
%{user_id: user_id} = step,
|
||||
attrs,
|
||||
%User{id: user_id}
|
||||
) do
|
||||
def update_changeset(%__MODULE__{} = step, attrs, %User{id: user_id})
|
||||
when user_id |> is_binary() do
|
||||
step
|
||||
|> cast(attrs, [:title, :content])
|
||||
|> validate_required([:title, :content, :user_id, :position])
|
||||
|> validate_required([:title, :user_id, :position])
|
||||
end
|
||||
|
||||
@spec position_changeset(t(), position :: non_neg_integer(), User.t()) :: changeset()
|
||||
def position_changeset(
|
||||
%{user_id: user_id} = step,
|
||||
position,
|
||||
%User{id: user_id}
|
||||
) do
|
||||
def position_changeset(%__MODULE__{} = step, position, %User{id: user_id})
|
||||
when user_id |> is_binary() do
|
||||
step
|
||||
|> change(position: position)
|
||||
|> validate_required([:title, :content, :user_id, :position])
|
||||
|> validate_required([:title, :user_id, :position])
|
||||
end
|
||||
end
|
||||
|
@ -3,9 +3,7 @@ defmodule Memex.Pipelines.Steps do
|
||||
The context for steps within a pipeline
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Ecto.Multi
|
||||
alias Memex.{Accounts.User, Repo}
|
||||
use Memex, :context
|
||||
alias Memex.Pipelines.{Pipeline, Steps.Step}
|
||||
|
||||
@doc """
|
||||
@ -21,11 +19,10 @@ defmodule Memex.Pipelines.Steps do
|
||||
|
||||
"""
|
||||
@spec list_steps(Pipeline.t(), User.t()) :: [Step.t()]
|
||||
def list_steps(%{id: pipeline_id}, %{id: user_id}) do
|
||||
def list_steps(%{id: pipeline_id}, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.all(
|
||||
from s in Step,
|
||||
where: s.pipeline_id == ^pipeline_id,
|
||||
where: s.user_id == ^user_id,
|
||||
order_by: s.position
|
||||
)
|
||||
end
|
||||
@ -62,8 +59,8 @@ defmodule Memex.Pipelines.Steps do
|
||||
|
||||
"""
|
||||
@spec get_step!(Step.id(), User.t()) :: Step.t()
|
||||
def get_step!(id, %{id: user_id}) do
|
||||
Repo.one!(from n in Step, where: n.id == ^id, where: n.user_id == ^user_id)
|
||||
def get_step!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||
Repo.one!(from n in Step, where: n.id == ^id)
|
||||
end
|
||||
|
||||
def get_step!(id, _invalid_user) do
|
||||
@ -119,22 +116,15 @@ defmodule Memex.Pipelines.Steps do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_step(%Step{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> delete_step(%Step{}, %User{role: :admin})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> delete_step(%Step{}, %User{id: 123})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> delete_step(%Step{}, nil)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_step(Step.t(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()}
|
||||
def delete_step(%Step{user_id: user_id} = step, %{id: user_id}) do
|
||||
delete_step(step)
|
||||
end
|
||||
|
||||
def delete_step(%Step{} = step, %{role: :admin}) do
|
||||
def delete_step(%Step{} = step, %{id: user_id}) when user_id |> is_binary() do
|
||||
delete_step(step)
|
||||
end
|
||||
|
||||
@ -181,10 +171,11 @@ defmodule Memex.Pipelines.Steps do
|
||||
def reorder_step(%Step{position: 0} = step, :up, _user), do: {:error, step}
|
||||
|
||||
def reorder_step(
|
||||
%Step{position: position, pipeline_id: pipeline_id, user_id: user_id} = step,
|
||||
%Step{position: position, pipeline_id: pipeline_id} = step,
|
||||
:up,
|
||||
%{id: user_id} = user
|
||||
) do
|
||||
)
|
||||
when user_id |> is_binary() do
|
||||
Multi.new()
|
||||
|> Multi.update_all(
|
||||
:reorder_steps,
|
||||
@ -207,10 +198,11 @@ defmodule Memex.Pipelines.Steps do
|
||||
end
|
||||
|
||||
def reorder_step(
|
||||
%Step{pipeline_id: pipeline_id, position: position, user_id: user_id} = step,
|
||||
%Step{pipeline_id: pipeline_id, position: position} = step,
|
||||
:down,
|
||||
%{id: user_id} = user
|
||||
) do
|
||||
)
|
||||
when user_id |> is_binary() do
|
||||
Multi.new()
|
||||
|> Multi.one(
|
||||
:step_count,
|
||||
|
@ -17,7 +17,7 @@ defmodule MemexWeb do
|
||||
those modules here.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(css js fonts images favicon.ico robots.txt)
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt webfonts)
|
||||
|
||||
def router do
|
||||
quote do
|
||||
@ -42,9 +42,10 @@ defmodule MemexWeb do
|
||||
formats: [:html, :json],
|
||||
layouts: [html: MemexWeb.Layouts]
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
|
||||
import MemexWeb.ControllerHelpers
|
||||
import Plug.Conn
|
||||
import MemexWeb.Gettext
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
@ -69,6 +70,7 @@ defmodule MemexWeb do
|
||||
|
||||
def html do
|
||||
quote do
|
||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
||||
use Phoenix.Component
|
||||
|
||||
# Import convenience functions from controllers
|
||||
@ -82,12 +84,10 @@ defmodule MemexWeb do
|
||||
|
||||
defp html_helpers do
|
||||
quote do
|
||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
||||
use Phoenix.HTML
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
||||
import Phoenix.Component
|
||||
import MemexWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers}
|
||||
use PhoenixHTMLHelpers
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
import Phoenix.{Component, HTML, HTML.Form}
|
||||
import MemexWeb.{ErrorHelpers, CoreComponents, HTMLHelpers}
|
||||
|
||||
# Shortcut for generating JS commands
|
||||
alias Phoenix.LiveView.JS
|
||||
|
@ -37,7 +37,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
||||
} = socket
|
||||
) do
|
||||
columns =
|
||||
if actions == [] or current_user |> is_nil() do
|
||||
if actions == [] or !current_user do
|
||||
[]
|
||||
else
|
||||
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
||||
@ -91,7 +91,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
||||
defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
|
||||
slug_block = ~H"""
|
||||
<.link navigate={~p"/context/#{@slug}"} class="link">
|
||||
<%= @slug %>
|
||||
{@slug}
|
||||
</.link>
|
||||
"""
|
||||
|
||||
@ -102,7 +102,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
||||
~H"""
|
||||
<div class="flex flex-wrap justify-center space-x-1">
|
||||
<.link :for={tag <- @tags} patch={~p"/contexts/#{tag}"} class="link">
|
||||
<%= tag %>
|
||||
{tag}
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
@ -113,7 +113,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
||||
|
||||
~H"""
|
||||
<div class="flex justify-center items-center space-x-4">
|
||||
<%= render_slot(@actions, @context) %>
|
||||
{render_slot(@actions, @context)}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
@ -2,13 +2,15 @@ defmodule MemexWeb.CoreComponents do
|
||||
@moduledoc """
|
||||
Provides core UI components.
|
||||
"""
|
||||
use PhoenixHTMLHelpers
|
||||
use Phoenix.Component
|
||||
use MemexWeb, :verified_routes
|
||||
import MemexWeb.{Gettext, HTMLHelpers}
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
import MemexWeb.HTMLHelpers
|
||||
alias Memex.{Accounts, Accounts.Invite, Accounts.User}
|
||||
alias Memex.Contexts.Context
|
||||
alias Memex.Notes.Note
|
||||
alias Memex.Pipelines.Steps.Step
|
||||
alias Memex.Pipelines.{Pipeline, Steps.Step}
|
||||
alias Phoenix.HTML
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@ -55,24 +57,6 @@ defmodule MemexWeb.CoreComponents do
|
||||
attr :id, :string, default: nil
|
||||
slot(:inner_block)
|
||||
|
||||
@doc """
|
||||
A toggle button element that can be directed to a liveview or a
|
||||
live_component's `handle_event/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.toggle_button action="my_liveview_action" value={@some_value}>
|
||||
<span>Toggle me!</span>
|
||||
</.toggle_button>
|
||||
<.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
|
||||
<span>Whatever you want</span>
|
||||
</.toggle_button>
|
||||
"""
|
||||
def toggle_button(assigns)
|
||||
|
||||
attr :user, User, required: true
|
||||
slot(:inner_block, required: true)
|
||||
|
||||
def user_card(assigns)
|
||||
|
||||
attr :invite, Invite, required: true
|
||||
@ -87,14 +71,14 @@ defmodule MemexWeb.CoreComponents do
|
||||
attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
|
||||
|
||||
@doc """
|
||||
Phoenix.Component for a <time> element that renders the naivedatetime in the
|
||||
Phoenix.Component for a <time> element that renders the DateTime in the
|
||||
user's local timezone
|
||||
"""
|
||||
def datetime(assigns)
|
||||
|
||||
@spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
|
||||
defp cast_datetime(%NaiveDateTime{} = datetime) do
|
||||
datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
|
||||
@spec cast_datetime(DateTime.t() | nil) :: String.t()
|
||||
defp cast_datetime(%DateTime{} = datetime) do
|
||||
datetime |> DateTime.to_iso8601(:extended)
|
||||
end
|
||||
|
||||
defp cast_datetime(_datetime), do: ""
|
||||
@ -130,53 +114,128 @@ defmodule MemexWeb.CoreComponents do
|
||||
|
||||
def step_content(assigns)
|
||||
|
||||
defp add_links_to_content(content, data_qa_prefix) do
|
||||
# replace links
|
||||
attr :pipeline, Pipeline, required: true
|
||||
|
||||
# link regex from
|
||||
# https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||
# and modified with additional schemes from
|
||||
# https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
||||
def pipeline_content(assigns)
|
||||
|
||||
content =
|
||||
Regex.replace(
|
||||
~r<((file|git|https?|ipfs|ipns|irc|jabber|magnet|mailto|mumble|tel|udp|xmpp):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))>,
|
||||
content,
|
||||
fn _whole_match, link ->
|
||||
link =
|
||||
HTML.Link.link(
|
||||
link,
|
||||
to: link,
|
||||
class: "link inline",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer"
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
defp display_links(record) do
|
||||
record
|
||||
|> get_content()
|
||||
|> Phoenix.HTML.html_escape()
|
||||
|> Phoenix.HTML.safe_to_string()
|
||||
|> replace_hyperlinks(record)
|
||||
|> replace_triple_links(record)
|
||||
|> replace_double_links(record)
|
||||
|> replace_single_links(record)
|
||||
|> HTML.raw()
|
||||
end
|
||||
|
||||
"</p>#{link}<p class=\"inline\">"
|
||||
end
|
||||
)
|
||||
defp get_content(%{content: content}), do: content |> get_text()
|
||||
defp get_content(%{description: description}), do: description |> get_text()
|
||||
defp get_content(_fallthrough), do: nil |> get_text()
|
||||
|
||||
content =
|
||||
Regex.replace(
|
||||
~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
|
||||
content,
|
||||
fn _whole_match, slug ->
|
||||
link =
|
||||
HTML.Link.link(
|
||||
"[[#{slug}]]",
|
||||
to: ~p"/note/#{slug}",
|
||||
class: "link inline",
|
||||
data: [qa: "#{data_qa_prefix}-#{slug}"]
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
defp get_text(string) when is_binary(string), do: string
|
||||
defp get_text(_fallthrough), do: ""
|
||||
|
||||
"</p>#{link}<p class=\"inline\">"
|
||||
end
|
||||
)
|
||||
# replaces hyperlinks like https://bubbletea.dev
|
||||
#
|
||||
# link regex from
|
||||
# https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||
# and modified with additional schemes from
|
||||
# https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
||||
defp replace_hyperlinks(content, _record) do
|
||||
Regex.replace(
|
||||
~r<((file|git|https?|ipfs|ipns|irc|jabber|magnet|mailto|mumble|tel|udp|xmpp):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))>,
|
||||
content,
|
||||
fn _whole_match, link ->
|
||||
link =
|
||||
link(
|
||||
link,
|
||||
to: link,
|
||||
class: "link inline break-words",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer"
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
content |> HTML.raw()
|
||||
"</p>#{link}<p class=\"inline break-words\">"
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
# replaces triple links like [[[slug-title]]]
|
||||
defp replace_triple_links(content, _record) do
|
||||
Regex.replace(
|
||||
~r/(^|[^\[])\[\[\[([\p{L}\p{N}\-]+)\]\]\]($|[^\]])/,
|
||||
content,
|
||||
fn _whole_match, prefix, slug, suffix ->
|
||||
link =
|
||||
link(
|
||||
"[[[#{slug}]]]",
|
||||
to: ~p"/note/#{slug}",
|
||||
class: "link inline break-words"
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
"#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
# replaces double links like [[slug-title]]
|
||||
defp replace_double_links(content, record) do
|
||||
Regex.replace(
|
||||
~r/(^|[^\[])\[\[([\p{L}\p{N}\-]+)\]\]($|[^\]])/,
|
||||
content,
|
||||
fn _whole_match, prefix, slug, suffix ->
|
||||
target =
|
||||
case record do
|
||||
%Pipeline{} -> ~p"/context/#{slug}"
|
||||
%Step{} -> ~p"/context/#{slug}"
|
||||
_context -> ~p"/note/#{slug}"
|
||||
end
|
||||
|
||||
link =
|
||||
link(
|
||||
"[[#{slug}]]",
|
||||
to: target,
|
||||
class: "link inline break-words"
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
"#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
# replaces single links like [slug-title]
|
||||
defp replace_single_links(content, record) do
|
||||
Regex.replace(
|
||||
~r/(^|[^\[])\[([\p{L}\p{N}\-]+)\]($|[^\]])/,
|
||||
content,
|
||||
fn _whole_match, prefix, slug, suffix ->
|
||||
target =
|
||||
case record do
|
||||
%Pipeline{} -> ~p"/pipeline/#{slug}"
|
||||
%Step{} -> ~p"/pipeline/#{slug}"
|
||||
%Context{} -> ~p"/context/#{slug}"
|
||||
_note -> ~p"/note/#{slug}"
|
||||
end
|
||||
|
||||
link =
|
||||
link(
|
||||
"[#{slug}]",
|
||||
to: target,
|
||||
class: "link inline break-words"
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
"#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,11 @@
|
||||
<div
|
||||
:if={@context.content}
|
||||
id={"show-context-content-#{@context.id}"}
|
||||
class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
|
||||
class="inline-block overflow-y-auto overflow-x-hidden whitespace-pre-wrap resize-y input input-primary h-128 min-h-128"
|
||||
phx-update="ignore"
|
||||
readonly
|
||||
phx-no-format
|
||||
><p class="inline"><%= add_links_to_content(@context.content, "context-note") %></p></div>
|
||||
><p class="inline"><%= display_links(@context) %></p></div>
|
||||
<div :if={!@context.content} class="text-sm italic text-center text-zinc-600">
|
||||
{gettext("(This context is empty)")}
|
||||
</div>
|
||||
|
@ -1,3 +1,3 @@
|
||||
<time :if={@date} id={@id} datetime={Date.to_iso8601(@date, :extended)} phx-hook="Date">
|
||||
<%= Date.to_iso8601(@date, :extended) %>
|
||||
{Date.to_iso8601(@date, :extended)}
|
||||
</time>
|
||||
|
@ -1,3 +1,3 @@
|
||||
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
|
||||
<%= cast_datetime(@datetime) %>
|
||||
{cast_datetime(@datetime)}
|
||||
</time>
|
||||
|
@ -1,25 +1,22 @@
|
||||
<div class="px-8 py-4 flex flex-col justify-center items-center space-y-4
|
||||
bg-primary-900
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out">
|
||||
<h1 class="title text-xl">
|
||||
<%= @invite.name %>
|
||||
<div class="flex flex-col justify-center items-center px-8 py-4 space-y-4 rounded-lg border border-zinc-400 shadow-lg transition-all duration-300 ease-in-out bg-primary-900 hover:shadow-md">
|
||||
<h1 class="text-xl title">
|
||||
{@invite.name}
|
||||
</h1>
|
||||
|
||||
<%= if @invite.disabled_at |> is_nil() do %>
|
||||
<%= if @invite.disabled_at do %>
|
||||
<h2 class="title text-md">
|
||||
<%= if @invite.uses_left do %>
|
||||
<%= gettext(
|
||||
"uses left: %{uses_left_count}",
|
||||
uses_left_count: @invite.uses_left
|
||||
) %>
|
||||
<% else %>
|
||||
<%= gettext("uses left: unlimited") %>
|
||||
<% end %>
|
||||
{gettext("invite disabled")}
|
||||
</h2>
|
||||
<% else %>
|
||||
<h2 class="title text-md">
|
||||
<%= gettext("invite disabled") %>
|
||||
<%= if @invite.uses_left do %>
|
||||
{gettext(
|
||||
"uses left: %{uses_left_count}",
|
||||
uses_left_count: @invite.uses_left
|
||||
)}
|
||||
<% else %>
|
||||
{gettext("uses left: unlimited")}
|
||||
<% end %>
|
||||
</h2>
|
||||
<% end %>
|
||||
|
||||
@ -29,20 +26,19 @@
|
||||
/>
|
||||
|
||||
<h2 :if={@use_count && @use_count != 0} class="title text-md">
|
||||
<%= gettext("uses: %{uses_count}", uses_count: @use_count) %>
|
||||
{gettext("uses: %{uses_count}", uses_count: @use_count)}
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-row flex-wrap justify-center items-center">
|
||||
<code
|
||||
id={"code-#{@invite.id}"}
|
||||
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all
|
||||
text-primary-400 bg-primary-800"
|
||||
class="px-4 py-2 mx-2 my-1 text-xs text-center break-all rounded-lg text-primary-400 bg-primary-800"
|
||||
phx-no-format
|
||||
><%= url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
|
||||
<%= if @code_actions, do: render_slot(@code_actions) %>
|
||||
{if @code_actions, do: render_slot(@code_actions)}
|
||||
</div>
|
||||
|
||||
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
<div :if={@inner_block} class="flex justify-center items-center space-x-4">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@
|
||||
patch={@return_to}
|
||||
id="close"
|
||||
class="absolute top-8 right-10
|
||||
text-gray-500 hover:text-gray-800
|
||||
text-zinc-500 hover:text-zinc-800
|
||||
transition-all duration-500 ease-in-out"
|
||||
phx-remove={hide_modal()}
|
||||
aria-label={gettext("close modal")}
|
||||
@ -37,7 +37,7 @@
|
||||
</.link>
|
||||
|
||||
<div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-stretch">
|
||||
<%= render_slot(@inner_block) %>
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,11 @@
|
||||
<div
|
||||
:if={@note.content}
|
||||
id={"show-note-content-#{@note.id}"}
|
||||
class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
|
||||
class="inline-block overflow-y-auto overflow-x-hidden whitespace-pre-wrap resize-y input input-primary h-128 min-h-128"
|
||||
phx-update="ignore"
|
||||
readonly
|
||||
phx-no-format
|
||||
><p class="inline"><%= add_links_to_content(@note.content, "note-link") %></p></div>
|
||||
><p class="inline"><%= display_links(@note) %></p></div>
|
||||
<div :if={!@note.content} class="text-sm italic text-center text-zinc-600">
|
||||
{gettext("(This note is empty)")}
|
||||
</div>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div
|
||||
:if={@pipeline.description}
|
||||
id={"show-pipeline-description-#{@pipeline.id}"}
|
||||
class="inline-block overflow-y-auto overflow-x-hidden h-32 whitespace-pre-wrap resize-y input input-primary min-h-32"
|
||||
phx-update="ignore"
|
||||
readonly
|
||||
phx-no-format
|
||||
><p class="inline"><%= display_links(@pipeline) %></p></div>
|
@ -1,7 +1,8 @@
|
||||
<div
|
||||
:if={@step.content}
|
||||
id={"show-step-content-#{@step.id}"}
|
||||
class="input input-primary h-32 min-h-32 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
|
||||
class="inline-block overflow-y-auto overflow-x-hidden h-32 whitespace-pre-wrap resize-y input input-primary min-h-32"
|
||||
phx-update="ignore"
|
||||
readonly
|
||||
phx-no-format
|
||||
><p class="inline"><%= add_links_to_content(@step.content, "step-context") %></p></div>
|
||||
><p class="inline"><%= display_links(@step) %></p></div>
|
||||
|
@ -1,30 +0,0 @@
|
||||
<label for={@id || @action} class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
id={@id || @action}
|
||||
type="checkbox"
|
||||
value={@value}
|
||||
checked={@value}
|
||||
class="sr-only peer"
|
||||
aria-labelledby={"#{@id || @action}-label"}
|
||||
{
|
||||
if assigns |> Map.has_key?(:target),
|
||||
do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
|
||||
else: %{"phx-click": @action, "phx-value-value": @value}
|
||||
}
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-300 rounded-full peer
|
||||
peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
|
||||
peer-checked:bg-gray-600
|
||||
peer-checked:after:translate-x-full peer-checked:after:border-white
|
||||
after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
|
||||
after:border after:rounded-full after:h-5 after:w-5
|
||||
after:transition-all after:duration-250 after:ease-in-out
|
||||
transition-colors duration-250 ease-in-out">
|
||||
</div>
|
||||
<span
|
||||
id={"#{@id || @action}-label"}
|
||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300 whitespace-nowrap"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</span>
|
||||
</label>
|
@ -2,14 +2,14 @@
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center">
|
||||
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
|
||||
<.link navigate={~p"/"} class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline">
|
||||
<%= gettext("memEx") %>
|
||||
{gettext("memEx")}
|
||||
</.link>
|
||||
|
||||
<%= if @title_content do %>
|
||||
<span class="mx-2 my-1">
|
||||
|
|
||||
</span>
|
||||
<%= @title_content %>
|
||||
{@title_content}
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@ -19,34 +19,34 @@
|
||||
text-lg text-primary-400 text-ellipsis">
|
||||
<li class="mx-2 my-1">
|
||||
<.link navigate={~p"/notes"} class="text-primary-400 hover:underline truncate">
|
||||
<%= gettext("notes") %>
|
||||
{gettext("notes")}
|
||||
</.link>
|
||||
</li>
|
||||
|
||||
<li class="mx-2 my-1">
|
||||
<.link navigate={~p"/contexts"} class="text-primary-400 hover:underline truncate">
|
||||
<%= gettext("contexts") %>
|
||||
{gettext("contexts")}
|
||||
</.link>
|
||||
</li>
|
||||
|
||||
<li class="mx-2 my-1">
|
||||
<.link navigate={~p"/pipelines"} class="text-primary-400 hover:underline truncate">
|
||||
<%= gettext("pipelines") %>
|
||||
{gettext("pipelines")}
|
||||
</.link>
|
||||
</li>
|
||||
|
||||
<li class="mx-2 my-1 border-left border border-primary-700"></li>
|
||||
|
||||
<%= if @current_user do %>
|
||||
<li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
|
||||
<li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1">
|
||||
<.link navigate={~p"/invites"} class="text-primary-400 hover:underline">
|
||||
<%= gettext("invites") %>
|
||||
{gettext("invites")}
|
||||
</.link>
|
||||
</li>
|
||||
|
||||
<li class="mx-2 my-1">
|
||||
<.link navigate={~p"/users/settings"} class="text-primary-400 hover:underline truncate">
|
||||
<%= @current_user.email %>
|
||||
{@current_user.email}
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
@ -76,13 +76,13 @@
|
||||
<% else %>
|
||||
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
||||
<.link href={~p"/users/register"} class="text-primary-400 hover:underline truncate">
|
||||
<%= dgettext("actions", "register") %>
|
||||
{dgettext("actions", "register")}
|
||||
</.link>
|
||||
</li>
|
||||
|
||||
<li class="mx-2 my-1">
|
||||
<.link href={~p"/users/log_in"} class="text-primary-400 hover:underline truncate">
|
||||
<%= dgettext("actions", "log in") %>
|
||||
{dgettext("actions", "log in")}
|
||||
</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
|
@ -2,36 +2,36 @@
|
||||
id={"user-#{@user.id}"}
|
||||
class="px-8 py-4 flex flex-col justify-center items-center text-center
|
||||
bg-primary-900
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
border border-zinc-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
|
||||
<%= @user.email %>
|
||||
{@user.email}
|
||||
</h1>
|
||||
|
||||
<h3 class="px-4 py-2 rounded-lg title text-lg">
|
||||
<p>
|
||||
<%= if @user.confirmed_at do %>
|
||||
<%= gettext(
|
||||
{gettext(
|
||||
"user confirmed on%{confirmed_datetime}",
|
||||
confirmed_datetime: ""
|
||||
) %>
|
||||
)}
|
||||
<.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
|
||||
<% else %>
|
||||
<%= gettext("email unconfirmed") %>
|
||||
{gettext("email unconfirmed")}
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= gettext(
|
||||
{gettext(
|
||||
"user registered on%{registered_datetime}",
|
||||
registered_datetime: ""
|
||||
) %>
|
||||
)}
|
||||
<.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
|
||||
</p>
|
||||
</h3>
|
||||
|
||||
<div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,18 +1,45 @@
|
||||
<main role="main" class="min-h-full min-w-full">
|
||||
<main role="main" class="pb-8 min-w-full">
|
||||
<header>
|
||||
<.topbar current_user={assigns[:current_user]} />
|
||||
|
||||
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
|
||||
<p :if={@flash["info"]} class="alert alert-info" role="alert">
|
||||
<%= @flash["info"] %>
|
||||
<p
|
||||
:if={@flash && @flash |> Map.has_key?("info")}
|
||||
class="alert alert-info cursor-pointer"
|
||||
role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="info"
|
||||
>
|
||||
{Phoenix.Flash.get(@flash, :info)}
|
||||
</p>
|
||||
<p :if={@flash["error"]} class="alert alert-danger" role="alert">
|
||||
<%= @flash["error"] %>
|
||||
|
||||
<p
|
||||
:if={@flash && @flash |> Map.has_key?("error")}
|
||||
class="alert alert-danger cursor-pointer"
|
||||
role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="error"
|
||||
>
|
||||
{Phoenix.Flash.get(@flash, :error)}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mx-4 sm:mx-8 md:mx-16">
|
||||
<%= @inner_content %>
|
||||
{@inner_content}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div
|
||||
id="disconnect"
|
||||
class="z-50 fixed opacity-0 bottom-8 right-12 px-8 py-4 w-max h-max
|
||||
border border-primary-400 shadow-lg rounded-lg bg-primary-900 text-primary-400
|
||||
flex justify-center items-center space-x-4
|
||||
transition-opacity ease-in-out duration-500 delay-[2000ms]"
|
||||
>
|
||||
<i class="fas fa-fade text-md fa-satellite-dish"></i>
|
||||
|
||||
<h1 class="title text-md">
|
||||
{gettext("Reconnecting...")}
|
||||
</h1>
|
||||
</div>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
<%= @email.subject %>
|
||||
{@email.subject}
|
||||
</title>
|
||||
</head>
|
||||
<body style="padding: 2em; color: rgb(161, 161, 170); background-color: rgb(39, 39, 42); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;">
|
||||
<%= @inner_content %>
|
||||
{@inner_content}
|
||||
|
||||
<hr style="margin: 2em auto; border-width: 1px; border-color: rgb(161, 161, 170); width: 100%; max-width: 42rem;" />
|
||||
|
||||
<a style="color: rgb(161, 161, 170);" href={~p"/"}>
|
||||
<%= dgettext("emails", "this email was sent from memEx") %>
|
||||
{dgettext("emails", "this email was sent from memEx")}
|
||||
</a>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1 +1 @@
|
||||
<%= @inner_content %>
|
||||
{@inner_content}
|
||||
|
@ -1,45 +0,0 @@
|
||||
<main class="pb-8 min-w-full">
|
||||
<header>
|
||||
<.topbar current_user={assigns[:current_user]} />
|
||||
|
||||
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
|
||||
<p
|
||||
:if={@flash && @flash |> Map.has_key?("info")}
|
||||
class="alert alert-info"
|
||||
role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="info"
|
||||
>
|
||||
<%= live_flash(@flash, "info") %>
|
||||
</p>
|
||||
|
||||
<p
|
||||
:if={@flash && @flash |> Map.has_key?("error")}
|
||||
class="alert alert-danger"
|
||||
role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="error"
|
||||
>
|
||||
<%= live_flash(@flash, "error") %>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mx-4 sm:mx-8 md:mx-16">
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div
|
||||
id="disconnect"
|
||||
class="z-50 fixed opacity-0 bottom-12 right-12 px-8 py-4 w-max h-max
|
||||
border border-primary-400 shadow-lg rounded-lg bg-primary-900 text-primary-400
|
||||
flex justify-center items-center space-x-4
|
||||
transition-opacity ease-in-out duration-500 delay-[2000ms]"
|
||||
>
|
||||
<i class="fas fa-fade text-md fa-satellite-dish"></i>
|
||||
|
||||
<h1 class="title text-md">
|
||||
<%= gettext("Reconnecting...") %>
|
||||
</h1>
|
||||
</div>
|
@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="m-0 p-0 w-full h-full bg-primary-800">
|
||||
<html lang="en" class="p-0 m-0 w-full h-full bg-primary-800 [scrollbar-gutter:stable]">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<%= csrf_meta_tag() %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content={get_csrf_token()} />
|
||||
<.live_title suffix={" | #{gettext("memEx")}"}>
|
||||
<%= assigns[:page_title] || gettext("memEx") %>
|
||||
{assigns[:page_title] || gettext("memEx")}
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/style.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="m-0 p-0 w-full h-full text-primary-400 subpixel-antialiased">
|
||||
<%= @inner_content %>
|
||||
<body class="p-0 m-0 w-full h-full subpixel-antialiased text-primary-400">
|
||||
{@inner_content}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -37,7 +37,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
||||
} = socket
|
||||
) do
|
||||
columns =
|
||||
if actions == [] or current_user |> is_nil() do
|
||||
if actions == [] or !current_user do
|
||||
[]
|
||||
else
|
||||
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
||||
@ -91,7 +91,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
||||
defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
|
||||
slug_block = ~H"""
|
||||
<.link navigate={~p"/note/#{@slug}"} class="link">
|
||||
<%= @slug %>
|
||||
{@slug}
|
||||
</.link>
|
||||
"""
|
||||
|
||||
@ -102,7 +102,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
||||
~H"""
|
||||
<div class="flex flex-wrap justify-center space-x-1">
|
||||
<.link :for={tag <- @tags} patch={~p"/notes/#{tag}"} class="link">
|
||||
<%= tag %>
|
||||
{tag}
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
@ -113,7 +113,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
||||
|
||||
~H"""
|
||||
<div class="flex justify-center items-center space-x-4">
|
||||
<%= render_slot(@actions, @note) %>
|
||||
{render_slot(@actions, @note)}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
@ -37,7 +37,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
||||
} = socket
|
||||
) do
|
||||
columns =
|
||||
if actions == [] or current_user |> is_nil() do
|
||||
if actions == [] or !current_user do
|
||||
[]
|
||||
else
|
||||
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
||||
@ -92,7 +92,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
||||
defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
|
||||
slug_block = ~H"""
|
||||
<.link navigate={~p"/pipeline/#{@slug}"} class="link">
|
||||
<%= @slug %>
|
||||
{@slug}
|
||||
</.link>
|
||||
"""
|
||||
|
||||
@ -101,8 +101,8 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
||||
|
||||
defp get_value_for_key(:description, %{description: description} = assigns, _additional_data) do
|
||||
description_block = ~H"""
|
||||
<div class="truncate max-w-sm">
|
||||
<%= @description %>
|
||||
<div class="max-w-sm truncate">
|
||||
{@description}
|
||||
</div>
|
||||
"""
|
||||
|
||||
@ -113,7 +113,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
||||
~H"""
|
||||
<div class="flex flex-wrap justify-center space-x-1">
|
||||
<.link :for={tag <- @tags} patch={~p"/pipelines/#{tag}"} class="link">
|
||||
<%= tag %>
|
||||
{tag}
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
@ -124,7 +124,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
||||
|
||||
~H"""
|
||||
<div class="flex justify-center items-center space-x-4">
|
||||
<%= render_slot(@actions, @pipeline) %>
|
||||
{render_slot(@actions, @pipeline)}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
@ -12,7 +12,7 @@
|
||||
phx-target={@myself}
|
||||
>
|
||||
<i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
|
||||
<span class={if @last_sort_key == key, do: "underline"}><%= label %></span>
|
||||
<span class={if @last_sort_key == key, do: "underline"}>{label}</span>
|
||||
<%= if @last_sort_key == key do %>
|
||||
<%= case @sort_mode do %>
|
||||
<% :asc -> %>
|
||||
@ -27,7 +27,7 @@
|
||||
</th>
|
||||
<% else %>
|
||||
<th class={["p-2 cursor-not-allowed", column[:class]]}>
|
||||
<%= label %>
|
||||
{label}
|
||||
</th>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@ -41,9 +41,9 @@
|
||||
<td :for={%{key: key} = value <- @columns} class={["p-2", value[:class]]}>
|
||||
<%= case values |> Map.get(key) do %>
|
||||
<% {_custom_sort_value, value} -> %>
|
||||
<%= value %>
|
||||
{value}
|
||||
<% value -> %>
|
||||
<%= value %>
|
||||
{value}
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
|
13
lib/memex_web/controller_helpers.ex
Normal file
13
lib/memex_web/controller_helpers.ex
Normal file
@ -0,0 +1,13 @@
|
||||
defmodule MemexWeb.ControllerHelpers do
|
||||
@moduledoc """
|
||||
Implements controller helpers
|
||||
"""
|
||||
|
||||
import Plug.Conn, only: [assign: 3]
|
||||
|
||||
def assign(conn, assigns) do
|
||||
assigns
|
||||
|> Map.new()
|
||||
|> Enum.reduce(conn, fn {key, value}, conn -> conn |> assign(key, value) end)
|
||||
end
|
||||
end
|
@ -1,23 +1,23 @@
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<span style="margin-bottom: 0.75em; font-size: 1.5em;">
|
||||
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
|
||||
{dgettext("emails", "Hi %{email},", email: @user.email)}
|
||||
</span>
|
||||
|
||||
<br />
|
||||
|
||||
<span style="margin-bottom: 1em; font-size: 1.25em;">
|
||||
<%= dgettext("emails", "Welcome to memEx") %>
|
||||
{dgettext("emails", "Welcome to memEx")}
|
||||
</span>
|
||||
|
||||
<br />
|
||||
|
||||
<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %>
|
||||
{dgettext("emails", "You can confirm your account by visiting the URL below:")}
|
||||
|
||||
<br />
|
||||
|
||||
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
|
||||
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}>{@url}</a>
|
||||
|
||||
<br />
|
||||
|
||||
<%= dgettext("emails", "If you didn't create an account at memEx, please ignore this.") %>
|
||||
{dgettext("emails", "If you didn't create an account at memEx, please ignore this.")}
|
||||
</div>
|
||||
|
@ -1,17 +1,17 @@
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
|
||||
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
|
||||
{dgettext("emails", "Hi %{email},", email: @user.email)}
|
||||
</span>
|
||||
|
||||
<br />
|
||||
|
||||
<%= dgettext("emails", "You can reset your password by visiting the URL below:") %>
|
||||
{dgettext("emails", "You can reset your password by visiting the URL below:")}
|
||||
|
||||
<br />
|
||||
|
||||
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
|
||||
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}>{@url}</a>
|
||||
|
||||
<br />
|
||||
|
||||
<%= dgettext("emails", "If you didn't request this change from memEx, please ignore this.") %>
|
||||
{dgettext("emails", "If you didn't request this change from memEx, please ignore this.")}
|
||||
</div>
|
||||
|
@ -1,20 +1,20 @@
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
|
||||
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
|
||||
{dgettext("emails", "Hi %{email},", email: @user.email)}
|
||||
</span>
|
||||
|
||||
<br />
|
||||
|
||||
<%= dgettext("emails", "You can change your email by visiting the URL below:") %>
|
||||
{dgettext("emails", "You can change your email by visiting the URL below:")}
|
||||
|
||||
<br />
|
||||
|
||||
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
|
||||
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}>{@url}</a>
|
||||
|
||||
<br />
|
||||
|
||||
<%= dgettext(
|
||||
{dgettext(
|
||||
"emails",
|
||||
"If you didn't request this change from memEx, please ignore this."
|
||||
) %>
|
||||
)}
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>
|
||||
<%= dgettext("errors", "Error") %> | <%= gettext("memEx") %>
|
||||
{dgettext("errors", "Error")} | {gettext("memEx")}
|
||||
</title>
|
||||
<link rel="stylesheet" href="/css/app.css" />
|
||||
<script defer type="text/javascript" src="/js/app.js">
|
||||
@ -19,13 +19,13 @@
|
||||
<div class="pb-8 w-full flex flex-col justify-center items-center text-center">
|
||||
<div class="p-8 sm:p-16 w-full flex flex-col justify-center items-center space-y-4 max-w-3xl">
|
||||
<h1 class="title text-primary-400 text-3xl">
|
||||
<%= @error_string %>
|
||||
{@error_string}
|
||||
</h1>
|
||||
|
||||
<hr class="w-full hr" />
|
||||
|
||||
<.link href={~p"/"} class="link title text-primary-400 text-lg">
|
||||
<%= dgettext("errors", "go back home") %>
|
||||
{dgettext("errors", "go back home")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
defmodule MemexWeb.ErrorJSON do
|
||||
import MemexWeb.Gettext
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
|
||||
def render(template, _assigns) do
|
||||
error_string =
|
||||
|
@ -4,9 +4,9 @@ defmodule MemexWeb.UserAuth do
|
||||
"""
|
||||
|
||||
use MemexWeb, :verified_routes
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
import MemexWeb.Gettext
|
||||
alias Memex.{Accounts, Accounts.User}
|
||||
|
||||
# Make the remember me cookie valid for 60 days.
|
||||
|
@ -1,7 +1,6 @@
|
||||
defmodule MemexWeb.UserConfirmationController do
|
||||
use MemexWeb, :controller
|
||||
|
||||
import MemexWeb.Gettext
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
alias Memex.Accounts
|
||||
|
||||
def new(conn, _params) do
|
||||
@ -43,7 +42,7 @@ defmodule MemexWeb.UserConfirmationController do
|
||||
# by some automation or by the user themselves, so we redirect without
|
||||
# a warning message.
|
||||
case conn.assigns do
|
||||
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
|
||||
%{current_user: %{confirmed_at: %{}}} ->
|
||||
redirect(conn, to: ~p"/")
|
||||
|
||||
%{} ->
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
||||
<h1 class="title text-primary-400 text-xl">
|
||||
<%= dgettext("actions", "Resend confirmation instructions") %>
|
||||
{dgettext("actions", "Resend confirmation instructions")}
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
@ -10,22 +10,22 @@
|
||||
action={~p"/users/confirm"}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-400") %>
|
||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
||||
{label(f, :email, gettext("Email"), class: "title text-lg text-primary-400")}
|
||||
{email_input(f, :email, required: true, class: "input input-primary col-span-2")}
|
||||
|
||||
<%= submit(dgettext("actions", "Resend confirmation instructions"),
|
||||
{submit(dgettext("actions", "Resend confirmation instructions"),
|
||||
class: "mx-auto btn btn-primary col-span-3"
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="flex flex-row justify-center items-center space-x-4">
|
||||
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "register") %>
|
||||
{dgettext("actions", "register")}
|
||||
</.link>
|
||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "log in") %>
|
||||
{dgettext("actions", "log in")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
defmodule MemexWeb.UserRegistrationController do
|
||||
use MemexWeb, :controller
|
||||
import MemexWeb.Gettext
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
alias Ecto.Changeset
|
||||
alias Memex.{Accounts, Accounts.Invites}
|
||||
|
||||
def new(conn, %{"invite" => invite_token}) do
|
||||
@ -69,8 +70,8 @@ defmodule MemexWeb.UserRegistrationController do
|
||||
|> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
|
||||
|> redirect(to: ~p"/")
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
conn |> render("new.html", changeset: changeset, invite_token: invite_token)
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
conn |> render(:new, changeset: changeset, invite_token: invite_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
||||
<h1 class="title text-primary-400 text-xl">
|
||||
<%= dgettext("actions", "register") %>
|
||||
{dgettext("actions", "register")}
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
@ -9,42 +9,42 @@
|
||||
action={~p"/users/register"}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
|
||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
||||
<p :if={@changeset.action && not @changeset.valid?} class="alert alert-danger col-span-3">
|
||||
{dgettext("errors", "oops, something went wrong! please check the errors below.")}
|
||||
</p>
|
||||
|
||||
<%= if @invite_token do %>
|
||||
<%= hidden_input(f, :invite_token, value: @invite_token) %>
|
||||
{hidden_input(f, :invite_token, value: @invite_token)}
|
||||
<% end %>
|
||||
|
||||
<%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
|
||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :email, "col-span-3") %>
|
||||
{label(f, :email, gettext("email"), class: "title text-lg text-primary-400")}
|
||||
{email_input(f, :email, required: true, class: "input input-primary col-span-2")}
|
||||
{error_tag(f, :email, "col-span-3")}
|
||||
|
||||
<%= label(f, :password, gettext("password"), class: "title text-lg text-primary-400") %>
|
||||
<%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :password, "col-span-3") %>
|
||||
{label(f, :password, gettext("password"), class: "title text-lg text-primary-400")}
|
||||
{password_input(f, :password, required: true, class: "input input-primary col-span-2")}
|
||||
{error_tag(f, :password, "col-span-3")}
|
||||
|
||||
<%= label(f, :locale, gettext("language"), class: "title text-lg text-primary-400") %>
|
||||
<%= select(
|
||||
{label(f, :locale, gettext("language"), class: "title text-lg text-primary-400")}
|
||||
{select(
|
||||
f,
|
||||
:locale,
|
||||
[{gettext("english"), "en_US"}],
|
||||
class: "input input-primary col-span-2"
|
||||
) %>
|
||||
<%= error_tag(f, :locale) %>
|
||||
)}
|
||||
{error_tag(f, :locale)}
|
||||
|
||||
<%= submit(dgettext("actions", "register"), class: "mx-auto btn btn-primary col-span-3") %>
|
||||
{submit(dgettext("actions", "register"), class: "mx-auto btn btn-primary col-span-3")}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="flex flex-row justify-center items-center space-x-4">
|
||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "log in") %>
|
||||
{dgettext("actions", "log in")}
|
||||
</.link>
|
||||
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "forgot your password?") %>
|
||||
{dgettext("actions", "forgot your password?")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,7 +40,7 @@ defmodule MemexWeb.UserResetPasswordController do
|
||||
# leaked token giving the user access to the account.
|
||||
def update(conn, %{"user" => user_params}) do
|
||||
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
||||
{:ok, _} ->
|
||||
{:ok, _user} ->
|
||||
conn
|
||||
|> put_flash(:info, dgettext("prompts", "password reset successfully."))
|
||||
|> redirect(to: ~p"/users/log_in")
|
||||
@ -54,7 +54,7 @@ defmodule MemexWeb.UserResetPasswordController do
|
||||
%{"token" => token} = conn.params
|
||||
|
||||
if user = Accounts.get_user_by_reset_password_token(token) do
|
||||
conn |> assign(:user, user) |> assign(:token, token)
|
||||
conn |> assign(user: user, token: token)
|
||||
else
|
||||
conn
|
||||
|> put_flash(
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
||||
<h1 class="title text-primary-400 text-xl">
|
||||
<%= dgettext("actions", "reset password") %>
|
||||
{dgettext("actions", "reset password")}
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
@ -9,36 +9,36 @@
|
||||
action={~p"/users/reset_password/#{@token}"}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
|
||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
||||
<p :if={@changeset.action && not @changeset.valid?} class="alert alert-danger col-span-3">
|
||||
{dgettext("errors", "oops, something went wrong! please check the errors below.")}
|
||||
</p>
|
||||
|
||||
<%= label(f, :password, gettext("new password"), class: "title text-lg text-primary-400") %>
|
||||
<%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :password, "col-span-3") %>
|
||||
{label(f, :password, gettext("new password"), class: "title text-lg text-primary-400")}
|
||||
{password_input(f, :password, required: true, class: "input input-primary col-span-2")}
|
||||
{error_tag(f, :password, "col-span-3")}
|
||||
|
||||
<%= label(f, :password_confirmation, gettext("confirm new password"),
|
||||
{label(f, :password_confirmation, gettext("confirm new password"),
|
||||
class: "title text-lg text-primary-400"
|
||||
) %>
|
||||
<%= password_input(f, :password_confirmation,
|
||||
)}
|
||||
{password_input(f, :password_confirmation,
|
||||
required: true,
|
||||
class: "input input-primary col-span-2"
|
||||
) %>
|
||||
<%= error_tag(f, :password_confirmation, "col-span-3") %>
|
||||
)}
|
||||
{error_tag(f, :password_confirmation, "col-span-3")}
|
||||
|
||||
<%= submit(dgettext("actions", "reset password"),
|
||||
{submit(dgettext("actions", "reset password"),
|
||||
class: "mx-auto btn btn-primary col-span-3"
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="flex flex-row justify-center items-center space-x-4">
|
||||
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "register") %>
|
||||
{dgettext("actions", "register")}
|
||||
</.link>
|
||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "log in") %>
|
||||
{dgettext("actions", "log in")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
||||
<h1 class="title text-primary-400 text-xl">
|
||||
<%= dgettext("actions", "forgot your password?") %>
|
||||
{dgettext("actions", "forgot your password?")}
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
@ -10,22 +10,22 @@
|
||||
action={~p"/users/reset_password"}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
|
||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
||||
{label(f, :email, gettext("email"), class: "title text-lg text-primary-400")}
|
||||
{email_input(f, :email, required: true, class: "input input-primary col-span-2")}
|
||||
|
||||
<%= submit(dgettext("actions", "send instructions to reset password"),
|
||||
{submit(dgettext("actions", "send instructions to reset password"),
|
||||
class: "mx-auto btn btn-primary col-span-3"
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="flex flex-row justify-center items-center space-x-4">
|
||||
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "register") %>
|
||||
{dgettext("actions", "register")}
|
||||
</.link>
|
||||
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "log in") %>
|
||||
{dgettext("actions", "log in")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
||||
<h1 class="title text-primary-400 text-xl">
|
||||
<%= dgettext("actions", "log in") %>
|
||||
{dgettext("actions", "log in")}
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
@ -11,31 +11,31 @@
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<p :if={@error_message} class="alert alert-danger col-span-3">
|
||||
<%= @error_message %>
|
||||
{@error_message}
|
||||
</p>
|
||||
|
||||
<%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
|
||||
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
|
||||
{label(f, :email, gettext("email"), class: "title text-lg text-primary-400")}
|
||||
{email_input(f, :email, required: true, class: "input input-primary col-span-2")}
|
||||
|
||||
<%= label(f, :password, gettext("password"), class: "title text-lg text-primary-400") %>
|
||||
<%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
|
||||
{label(f, :password, gettext("password"), class: "title text-lg text-primary-400")}
|
||||
{password_input(f, :password, required: true, class: "input input-primary col-span-2")}
|
||||
|
||||
<%= label(f, :remember_me, gettext("keep me logged in for 60 days"),
|
||||
{label(f, :remember_me, gettext("keep me logged in for 60 days"),
|
||||
class: "title text-lg text-primary-400"
|
||||
) %>
|
||||
<%= checkbox(f, :remember_me, class: "checkbox col-span-2") %>
|
||||
)}
|
||||
{checkbox(f, :remember_me, class: "checkbox col-span-2")}
|
||||
|
||||
<%= submit(dgettext("actions", "log in"), class: "mx-auto btn btn-primary col-span-3") %>
|
||||
{submit(dgettext("actions", "log in"), class: "mx-auto btn btn-primary col-span-3")}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="flex flex-row justify-center items-center space-x-4">
|
||||
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "register") %>
|
||||
{dgettext("actions", "register")}
|
||||
</.link>
|
||||
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "forgot your password?") %>
|
||||
{dgettext("actions", "forgot your password?")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
defmodule MemexWeb.UserSettingsController do
|
||||
use MemexWeb, :controller
|
||||
import MemexWeb.Gettext
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
alias Memex.Accounts
|
||||
alias MemexWeb.UserAuth
|
||||
|
||||
@ -103,8 +103,10 @@ defmodule MemexWeb.UserSettingsController do
|
||||
|
||||
defp assign_email_and_password_changesets(%{assigns: %{current_user: user}} = conn, _opts) do
|
||||
conn
|
||||
|> assign(:email_changeset, Accounts.change_user_email(user))
|
||||
|> assign(:password_changeset, Accounts.change_user_password(user))
|
||||
|> assign(:locale_changeset, Accounts.change_user_locale(user))
|
||||
|> assign(
|
||||
email_changeset: Accounts.change_user_email(user),
|
||||
locale_changeset: Accounts.change_user_locale(user),
|
||||
password_changeset: Accounts.change_user_password(user)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto pb-8 max-w-3xl flex flex-col justify-center items-stretch text-right space-y-4">
|
||||
<h1 class="title text-primary-400 text-xl text-left">
|
||||
<%= gettext("settings") %>
|
||||
{gettext("settings")}
|
||||
</h1>
|
||||
|
||||
<hr class="hr" />
|
||||
@ -12,37 +12,37 @@
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<h3 class="title text-primary-400 text-lg text-center col-span-3">
|
||||
<%= dgettext("actions", "change email") %>
|
||||
{dgettext("actions", "change email")}
|
||||
</h3>
|
||||
|
||||
<div
|
||||
:if={@email_changeset.action && not @email_changeset.valid?()}
|
||||
:if={@email_changeset.action && not @email_changeset.valid?}
|
||||
class="alert alert-danger col-span-3"
|
||||
>
|
||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below") %>
|
||||
{dgettext("errors", "oops, something went wrong! please check the errors below")}
|
||||
</div>
|
||||
|
||||
<%= hidden_input(f, :action, name: "action", value: "update_email") %>
|
||||
{hidden_input(f, :action, name: "action", value: "update_email")}
|
||||
|
||||
<%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
|
||||
<%= email_input(f, :email, required: true, class: "mx-2 my-1 input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :email, "col-span-3") %>
|
||||
{label(f, :email, gettext("email"), class: "title text-lg text-primary-400")}
|
||||
{email_input(f, :email, required: true, class: "mx-2 my-1 input input-primary col-span-2")}
|
||||
{error_tag(f, :email, "col-span-3")}
|
||||
|
||||
<%= label(f, :current_password, gettext("current password"),
|
||||
{label(f, :current_password, gettext("current password"),
|
||||
for: "current_password_for_email",
|
||||
class: "mx-2 my-1 title text-lg text-primary-400"
|
||||
) %>
|
||||
<%= password_input(f, :current_password,
|
||||
)}
|
||||
{password_input(f, :current_password,
|
||||
required: true,
|
||||
name: "current_password",
|
||||
id: "current_password_for_email",
|
||||
class: "mx-2 my-1 input input-primary col-span-2"
|
||||
) %>
|
||||
<%= error_tag(f, :current_password, "col-span-3") %>
|
||||
)}
|
||||
{error_tag(f, :current_password, "col-span-3")}
|
||||
|
||||
<%= submit(dgettext("actions", "change email"),
|
||||
{submit(dgettext("actions", "change email"),
|
||||
class: "mx-auto btn btn-primary col-span-3"
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
@ -54,49 +54,49 @@
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<h3 class="title text-primary-400 text-lg text-center col-span-3">
|
||||
<%= dgettext("actions", "change password") %>
|
||||
{dgettext("actions", "change password")}
|
||||
</h3>
|
||||
|
||||
<p
|
||||
:if={@password_changeset.action && not @password_changeset.valid?()}
|
||||
:if={@password_changeset.action && not @password_changeset.valid?}
|
||||
class="alert alert-danger col-span-3"
|
||||
>
|
||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
||||
{dgettext("errors", "oops, something went wrong! please check the errors below.")}
|
||||
</p>
|
||||
|
||||
<%= hidden_input(f, :action, name: "action", value: "update_password") %>
|
||||
{hidden_input(f, :action, name: "action", value: "update_password")}
|
||||
|
||||
<%= label(f, :password, gettext("new password"), class: "title text-lg text-primary-400") %>
|
||||
<%= password_input(f, :password,
|
||||
{label(f, :password, gettext("new password"), class: "title text-lg text-primary-400")}
|
||||
{password_input(f, :password,
|
||||
required: true,
|
||||
class: "mx-2 my-1 input input-primary col-span-2"
|
||||
) %>
|
||||
<%= error_tag(f, :password, "col-span-3") %>
|
||||
)}
|
||||
{error_tag(f, :password, "col-span-3")}
|
||||
|
||||
<%= label(f, :password_confirmation, gettext("confirm new password"),
|
||||
{label(f, :password_confirmation, gettext("confirm new password"),
|
||||
class: "title text-lg text-primary-400"
|
||||
) %>
|
||||
<%= password_input(f, :password_confirmation,
|
||||
)}
|
||||
{password_input(f, :password_confirmation,
|
||||
required: true,
|
||||
class: "mx-2 my-1 input input-primary col-span-2"
|
||||
) %>
|
||||
<%= error_tag(f, :password_confirmation, "col-span-3") %>
|
||||
)}
|
||||
{error_tag(f, :password_confirmation, "col-span-3")}
|
||||
|
||||
<%= label(f, :current_password, gettext("current password"),
|
||||
{label(f, :current_password, gettext("current password"),
|
||||
for: "current_password_for_password",
|
||||
class: "title text-lg text-primary-400"
|
||||
) %>
|
||||
<%= password_input(f, :current_password,
|
||||
)}
|
||||
{password_input(f, :current_password,
|
||||
required: true,
|
||||
name: "current_password",
|
||||
id: "current_password_for_password",
|
||||
class: "mx-2 my-1 input input-primary col-span-2"
|
||||
) %>
|
||||
<%= error_tag(f, :current_password, "col-span-3") %>
|
||||
)}
|
||||
{error_tag(f, :current_password, "col-span-3")}
|
||||
|
||||
<%= submit(dgettext("actions", "change password"),
|
||||
{submit(dgettext("actions", "change password"),
|
||||
class: "mx-auto btn btn-primary col-span-3"
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
@ -107,35 +107,35 @@
|
||||
action={~p"/users/settings"}
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
>
|
||||
<%= label(f, :locale, dgettext("actions", "change language"),
|
||||
{label(f, :locale, dgettext("actions", "change language"),
|
||||
class: "title text-primary-400 text-lg text-center col-span-3"
|
||||
) %>
|
||||
)}
|
||||
|
||||
<div
|
||||
:if={@locale_changeset.action && not @locale_changeset.valid?()}
|
||||
:if={@locale_changeset.action && not @locale_changeset.valid?}
|
||||
class="alert alert-danger col-span-3"
|
||||
>
|
||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below") %>
|
||||
{dgettext("errors", "oops, something went wrong! please check the errors below")}
|
||||
</div>
|
||||
|
||||
<%= hidden_input(f, :action, name: "action", value: "update_locale") %>
|
||||
{hidden_input(f, :action, name: "action", value: "update_locale")}
|
||||
|
||||
<%= select(f, :locale, [{gettext("english"), "en_US"}, {"spanish", "es"}],
|
||||
{select(f, :locale, [{gettext("english"), "en_US"}, {"spanish", "es"}],
|
||||
class: "mx-2 my-1 min-w-md input input-primary col-start-2"
|
||||
) %>
|
||||
<%= error_tag(f, :locale, "col-span-3") %>
|
||||
)}
|
||||
{error_tag(f, :locale, "col-span-3")}
|
||||
|
||||
<%= submit(dgettext("actions", "change language"),
|
||||
{submit(dgettext("actions", "change language"),
|
||||
class: "whitespace-nowrap mx-auto btn btn-primary col-span-3",
|
||||
data: [qa: dgettext("prompts", "are you sure you want to change your language?")]
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<div class="flex justify-end items-center">
|
||||
<.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
|
||||
<%= dgettext("actions", "export data as json") %>
|
||||
{dgettext("actions", "export data as json")}
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
@ -144,7 +144,7 @@
|
||||
class="mx-4 my-2 btn btn-alert"
|
||||
data-confirm={dgettext("prompts", "are you sure you want to delete your account?")}
|
||||
>
|
||||
<%= dgettext("actions", "delete user") %>
|
||||
{dgettext("actions", "delete user")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ defmodule MemexWeb.ErrorHelpers do
|
||||
Conveniences for translating and building error messages.
|
||||
"""
|
||||
|
||||
use Phoenix.HTML
|
||||
use PhoenixHTMLHelpers
|
||||
import Phoenix.Component
|
||||
alias Ecto.Changeset
|
||||
alias Phoenix.{HTML.Form, LiveView.Rendered}
|
||||
@ -19,10 +19,10 @@ defmodule MemexWeb.ErrorHelpers do
|
||||
~H"""
|
||||
<span
|
||||
:for={error <- Keyword.get_values(@form.errors, @field)}
|
||||
:if={used_input?(@form[@field])}
|
||||
class={["invalid-feedback", @extra_class]}
|
||||
phx-feedback-for={input_name(@form, @field)}
|
||||
>
|
||||
<%= translate_error(error) %>
|
||||
{translate_error(error)}
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
@ -5,7 +5,7 @@ defmodule MemexWeb.Gettext do
|
||||
By using [Gettext](https://hexdocs.pm/gettext),
|
||||
your module gains a set of macros for translations, for example:
|
||||
|
||||
import MemexWeb.Gettext
|
||||
use Gettext, backend: MemexWeb.Gettext
|
||||
|
||||
# Simple translation
|
||||
gettext("Here is the string to translate")
|
||||
@ -20,5 +20,5 @@ defmodule MemexWeb.Gettext do
|
||||
|
||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||
"""
|
||||
use Gettext, otp_app: :memex
|
||||
use Gettext.Backend, otp_app: :memex
|
||||
end
|
||||
|
@ -69,7 +69,7 @@ defmodule MemexWeb.ContextLive.FormComponent do
|
||||
|> push_navigate(to: return_to)
|
||||
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
assign(socket, changeset: changeset)
|
||||
assign(socket, :changeset, changeset)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
|
||||
<div class="flex flex-col justify-start items-stretch space-y-4 h-full">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
@ -6,45 +6,54 @@
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-debounce="300"
|
||||
phx-hook="CtrlEnter"
|
||||
class="flex flex-col justify-start items-stretch space-y-4"
|
||||
>
|
||||
<%= text_input(f, :slug,
|
||||
{text_input(f, :slug,
|
||||
aria_label: gettext("slug"),
|
||||
class: "input input-primary",
|
||||
phx_debounce: 300,
|
||||
phx_hook: "SanitizeTitles",
|
||||
placeholder: gettext("slug"),
|
||||
aria_label: gettext("slug")
|
||||
) %>
|
||||
<%= error_tag(f, :slug) %>
|
||||
required: true
|
||||
)}
|
||||
{error_tag(f, :slug)}
|
||||
|
||||
<%= textarea(f, :content,
|
||||
{textarea(f, :content,
|
||||
id: "context-form-content",
|
||||
class: "input input-primary h-64 min-h-64",
|
||||
phx_update: "ignore",
|
||||
placeholder: gettext("use [[note-slug]] to link to a note"),
|
||||
aria_label: gettext("use [[note-slug]] to link to a note")
|
||||
) %>
|
||||
<%= error_tag(f, :content) %>
|
||||
placeholder:
|
||||
gettext("use [[note-slug]] to link to a note or [context-slug] to link to a context"),
|
||||
aria_label:
|
||||
gettext("use [[note-slug]] to link to a note or [context-slug] to link to a context"),
|
||||
phx_debounce: 300
|
||||
)}
|
||||
{error_tag(f, :content)}
|
||||
|
||||
<%= text_input(f, :tags_string,
|
||||
id: "tags-input",
|
||||
{text_input(f, :tags_string,
|
||||
aria_label: gettext("tag1,tag2"),
|
||||
class: "input input-primary",
|
||||
placeholder: gettext("tag1,tag2"),
|
||||
aria_label: gettext("tag1,tag2")
|
||||
) %>
|
||||
<%= error_tag(f, :tags_string) %>
|
||||
id: "tags-input",
|
||||
phx_debounce: 300,
|
||||
phx_hook: "SanitizeTags",
|
||||
placeholder: gettext("tag1,tag2")
|
||||
)}
|
||||
{error_tag(f, :tags_string)}
|
||||
|
||||
<div class="flex justify-center items-stretch space-x-4">
|
||||
<%= select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility),
|
||||
{select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility),
|
||||
class: "grow input input-primary",
|
||||
prompt: gettext("select privacy"),
|
||||
aria_label: gettext("select privacy")
|
||||
) %>
|
||||
aria_label: gettext("select privacy"),
|
||||
phx_debounce: 300
|
||||
)}
|
||||
|
||||
<%= submit(dgettext("actions", "save"),
|
||||
{submit(dgettext("actions", "save"),
|
||||
phx_disable_with: gettext("saving..."),
|
||||
class: "mx-auto btn btn-primary"
|
||||
) %>
|
||||
)}
|
||||
</div>
|
||||
<%= error_tag(f, :visibility) %>
|
||||
{error_tag(f, :visibility)}
|
||||
</.form>
|
||||
</div>
|
||||
|
@ -20,29 +20,37 @@ defmodule MemexWeb.ContextLive.Index do
|
||||
%{slug: slug} = context = Contexts.get_context_by_slug(slug, current_user)
|
||||
|
||||
socket
|
||||
|> assign(page_title: gettext("edit %{slug}", slug: slug))
|
||||
|> assign(context: context)
|
||||
|> assign(
|
||||
context: context,
|
||||
page_title: gettext("edit %{slug}", slug: slug)
|
||||
)
|
||||
end
|
||||
|
||||
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
|
||||
socket
|
||||
|> assign(page_title: gettext("new context"))
|
||||
|> assign(context: %Context{visibility: :private, user_id: current_user_id})
|
||||
|> assign(
|
||||
context: %Context{visibility: :private, user_id: current_user_id},
|
||||
page_title: gettext("new context")
|
||||
)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(page_title: gettext("contexts"))
|
||||
|> assign(search: nil)
|
||||
|> assign(context: nil)
|
||||
|> assign(
|
||||
context: nil,
|
||||
page_title: gettext("contexts"),
|
||||
search: nil
|
||||
)
|
||||
|> display_contexts()
|
||||
end
|
||||
|
||||
defp apply_action(socket, :search, %{"search" => search}) do
|
||||
socket
|
||||
|> assign(page_title: gettext("contexts"))
|
||||
|> assign(search: search)
|
||||
|> assign(context: nil)
|
||||
|> assign(
|
||||
context: nil,
|
||||
page_title: gettext("contexts"),
|
||||
search: search
|
||||
)
|
||||
|> display_contexts()
|
||||
end
|
||||
|
||||
@ -68,8 +76,7 @@ defmodule MemexWeb.ContextLive.Index do
|
||||
{:noreply, socket |> push_patch(to: redirect_to)}
|
||||
end
|
||||
|
||||
defp display_contexts(%{assigns: %{current_user: current_user, search: search}} = socket)
|
||||
when not (current_user |> is_nil()) do
|
||||
defp display_contexts(%{assigns: %{current_user: %{} = current_user, search: search}} = socket) do
|
||||
socket |> assign(contexts: Contexts.list_contexts(search, current_user))
|
||||
end
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto flex flex-col justify-center items-start space-y-4 max-w-3xl">
|
||||
<div class="flex flex-col justify-center items-start mx-auto space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl">
|
||||
<%= gettext("contexts") %>
|
||||
{gettext("contexts")}
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
@ -9,20 +9,20 @@
|
||||
as={:search}
|
||||
phx-change="search"
|
||||
phx-submit="search"
|
||||
class="self-stretch flex flex-col items-stretch"
|
||||
class="flex flex-col items-stretch self-stretch"
|
||||
>
|
||||
<%= text_input(f, :search_term,
|
||||
{text_input(f, :search_term,
|
||||
class: "input input-primary",
|
||||
value: @search,
|
||||
role: "search",
|
||||
phx_debounce: 300,
|
||||
placeholder: gettext("search")
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
|
||||
<%= if @contexts |> Enum.empty?() do %>
|
||||
<h1 class="self-center text-primary-500">
|
||||
<%= gettext("no contexts found") %>
|
||||
{gettext("no contexts found")}
|
||||
</h1>
|
||||
<% else %>
|
||||
<.live_component
|
||||
@ -33,28 +33,28 @@
|
||||
>
|
||||
<:actions :let={context}>
|
||||
<.link
|
||||
:if={Contexts.is_owner?(context, @current_user)}
|
||||
:if={@current_user}
|
||||
patch={~p"/contexts/#{context}/edit"}
|
||||
aria-label={dgettext("actions", "edit %{context_slug}", context_slug: context.slug)}
|
||||
>
|
||||
<%= dgettext("actions", "edit") %>
|
||||
{dgettext("actions", "edit")}
|
||||
</.link>
|
||||
<.link
|
||||
:if={Contexts.is_owner_or_admin?(context, @current_user)}
|
||||
:if={@current_user}
|
||||
href="#"
|
||||
phx-click="delete"
|
||||
phx-value-id={context.id}
|
||||
data-confirm={dgettext("prompts", "are you sure?")}
|
||||
aria-label={dgettext("actions", "delete %{context_slug}", context_slug: context.slug)}
|
||||
>
|
||||
<%= dgettext("actions", "delete") %>
|
||||
{dgettext("actions", "delete")}
|
||||
</.link>
|
||||
</:actions>
|
||||
</.live_component>
|
||||
<% end %>
|
||||
|
||||
<.link :if={@current_user} patch={~p"/contexts/new"} class="self-end btn btn-primary">
|
||||
<%= dgettext("actions", "new context") %>
|
||||
{dgettext("actions", "new context")}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
defmodule MemexWeb.ContextLive.Show do
|
||||
use MemexWeb, :live_view
|
||||
alias Memex.Contexts
|
||||
alias Memex.{Contexts, Pipelines}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
@ -21,8 +21,12 @@ defmodule MemexWeb.ContextLive.Show do
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, page_title(live_action, context))
|
||||
|> assign(:context, context)
|
||||
|> assign(
|
||||
context: context,
|
||||
page_title: page_title(live_action, context),
|
||||
context_backlinks: Contexts.backlink("[#{context.slug}]", current_user),
|
||||
pipeline_backlinks: Pipelines.backlink("[[#{context.slug}]]", current_user)
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
@ -1,37 +1,54 @@
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
|
||||
<div class="flex flex-col justify-center items-stretch mx-auto space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl">
|
||||
<%= @context.slug %>
|
||||
{@context.slug}
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-wrap space-x-1">
|
||||
<.link :for={tag <- @context.tags} navigate={~p"/contexts/#{tag}"} class="link">
|
||||
<%= tag %>
|
||||
{tag}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<.context_content context={@context} />
|
||||
|
||||
<div
|
||||
:if={@context_backlinks ++ @pipeline_backlinks != []}
|
||||
class="flex flex-wrap justify-end items-center self-end"
|
||||
>
|
||||
<p>{gettext("Backlinked by:")}</p>
|
||||
<.link
|
||||
:for={backlink <- @context_backlinks}
|
||||
class="m-1 hover:underline"
|
||||
patch={~p"/context/#{backlink}"}
|
||||
>
|
||||
{gettext("[%{slug}]", slug: backlink.slug)}
|
||||
</.link>
|
||||
<.link
|
||||
:for={backlink <- @pipeline_backlinks}
|
||||
class="m-1 hover:underline"
|
||||
patch={~p"/pipeline/#{backlink}"}
|
||||
>
|
||||
{gettext("[[%{slug}]]", slug: backlink.slug)}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<p class="self-end">
|
||||
<%= gettext("Visibility: %{visibility}", visibility: @context.visibility) %>
|
||||
{gettext("Visibility: %{visibility}", visibility: @context.visibility)}
|
||||
</p>
|
||||
|
||||
<div class="self-end flex space-x-4">
|
||||
<.link
|
||||
:if={Contexts.is_owner?(@context, @current_user)}
|
||||
class="btn btn-primary"
|
||||
patch={~p"/context/#{@context}/edit"}
|
||||
>
|
||||
<%= dgettext("actions", "edit") %>
|
||||
<div class="flex self-end space-x-4">
|
||||
<.link :if={@current_user} class="btn btn-primary" patch={~p"/context/#{@context}/edit"}>
|
||||
{dgettext("actions", "edit")}
|
||||
</.link>
|
||||
<button
|
||||
:if={Contexts.is_owner_or_admin?(@context, @current_user)}
|
||||
:if={@current_user}
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
phx-click="delete"
|
||||
data-confirm={dgettext("prompts", "are you sure?")}
|
||||
aria-label={dgettext("actions", "delete %{context_slug}", context_slug: @context.slug)}
|
||||
>
|
||||
<%= dgettext("actions", "delete") %>
|
||||
{dgettext("actions", "delete")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-8 text-center max-w-3xl">
|
||||
<h1 class="title text-primary-400 text-xl text-left">
|
||||
<%= gettext("faq") %>
|
||||
{gettext("faq")}
|
||||
</h1>
|
||||
|
||||
<hr class="hr" />
|
||||
@ -8,13 +8,13 @@
|
||||
<ul class="flex flex-col justify-center items-stretch space-y-8">
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap text-left">
|
||||
<%= gettext("what is this?") %>
|
||||
{gettext("what is this?")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
{gettext(
|
||||
"this is a memex, used to document not just your notes, but also your perspectives and processes."
|
||||
) %>
|
||||
<%= gettext("some things that this memex is very loosely inspired by:") %>
|
||||
)}
|
||||
{gettext("some things that this memex is very loosely inspired by:")}
|
||||
</p>
|
||||
|
||||
<ul class="list-disc flex flex-col justify-center items-center space-y-2">
|
||||
@ -25,7 +25,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<%= gettext("memex") %>
|
||||
{gettext("memex")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
@ -35,7 +35,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<%= gettext("zettelkasten") %>
|
||||
{gettext("zettelkasten")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
@ -45,7 +45,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<%= gettext("org-mode") %>
|
||||
{gettext("org-mode")}
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
@ -53,75 +53,75 @@
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap text-left">
|
||||
<%= gettext("why split up into notes, contexts and pipelines?") %>
|
||||
{gettext("why split up into notes, contexts and pipelines?")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
{gettext(
|
||||
"i really admired the idea of a zettelkasten, especially with org-mode backlinks, however I felt like my notes would immediately become too messy by just putting everything into a single hierarchy."
|
||||
) %>
|
||||
<%= gettext(
|
||||
)}
|
||||
{gettext(
|
||||
"i wanted to separate between a personal dictionary of concepts and then my thought processes that are built off of my experiences and life lessons. these are notes, and contexts, respectively."
|
||||
) %>
|
||||
<%= gettext(
|
||||
)}
|
||||
{gettext(
|
||||
"finally, i wanted to externalize the processes for common situations that use these thought processes at discrete steps. these are pipelines!"
|
||||
) %>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap text-left">
|
||||
<%= gettext("what should my notes be like?") %>
|
||||
{gettext("what should my notes be like?")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
{gettext(
|
||||
"in my opinion, notes should be written by any of the discrete objects or concepts that are meaningful to you in your life."
|
||||
) %>
|
||||
<%= gettext(
|
||||
)}
|
||||
{gettext(
|
||||
"spoons? probably not. a particular brand of spoons that you really like? why not :)"
|
||||
) %>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap text-left">
|
||||
<%= gettext("what should my contexts be like?") %>
|
||||
{gettext("what should my contexts be like?")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("in my opinion, contexts should be like single-topic blog posts.") %>
|
||||
<%= gettext(
|
||||
"for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
|
||||
) %>
|
||||
{gettext("in my opinion, contexts should be like single-topic blog posts.")}
|
||||
{gettext(
|
||||
"for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could link to the spoon note as an example of how it fits nicely into your hand."
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap text-left">
|
||||
<%= gettext("what should my pipelines be like?") %>
|
||||
{gettext("what should my pipelines be like?")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
|
||||
) %>
|
||||
<%= gettext(
|
||||
"for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
|
||||
) %>
|
||||
{gettext(
|
||||
"in my opinion, pipelines should be pretty lightweight, and just link to contexts to provide most of the heavy lifting."
|
||||
)}
|
||||
{gettext(
|
||||
"for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could link to the physical designs context, maybe with some notes about how it applies in this case."
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap text-left">
|
||||
<%= gettext("how many people should i invite?") %>
|
||||
{gettext("how many people should i invite?")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
{gettext(
|
||||
"while memEx fully supports multiple users, each memEx instance should be treated as a single cohesive and collaborative document."
|
||||
) %>
|
||||
<%= gettext(
|
||||
"note, context and pipeline slugs must be unique, and you are free to backlink to notes not written by you."
|
||||
) %>
|
||||
<%= gettext(
|
||||
)}
|
||||
{gettext(
|
||||
"note, context and pipeline slugs must be unique, and you are free to link to notes not written by you."
|
||||
)}
|
||||
{gettext(
|
||||
"so, i'd recommend inviting anyone you'd like to work on your collective memEx. however, when in doubt, hopefully setting up a new instance is easy enough. if it isn't, then feel free to let me know :)"
|
||||
) %>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -11,6 +11,6 @@ defmodule MemexWeb.HomeLive do
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
admins = Accounts.list_users_by_role(:admin)
|
||||
{:ok, socket |> assign(page_title: gettext("home"), admins: admins, version: @version)}
|
||||
{:ok, socket |> assign(admins: admins, page_title: gettext("home"), version: @version)}
|
||||
end
|
||||
end
|
||||
|
@ -1,39 +1,39 @@
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-lg">
|
||||
<h1 class="title text-primary-400 text-xl">
|
||||
<%= gettext("memEx") %>
|
||||
<div class="flex flex-col justify-center items-stretch mx-auto space-y-4 max-w-lg">
|
||||
<h1 class="text-xl title text-primary-400">
|
||||
{gettext("memEx")}
|
||||
</h1>
|
||||
|
||||
<ul class="flex flex-col space-y-4">
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("notes:") %>
|
||||
{gettext("notes:")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("document notes about individual items or concepts") %>
|
||||
{gettext("document notes about individual items or concepts")}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("contexts:") %>
|
||||
{gettext("contexts:")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("provide context around a single topic and hotlink to your notes") %>
|
||||
{gettext("provide context around a single topic and hotlink to your notes")}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("pipelines:") %>
|
||||
{gettext("pipelines:")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("document your processes, attaching contexts to each step") %>
|
||||
{gettext("document your processes, attaching contexts to each step")}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center text-right space-y-2">
|
||||
<li class="flex flex-col justify-center items-center space-y-2 text-right">
|
||||
<.link navigate={~p"/faq"} class="btn btn-primary">
|
||||
<%= gettext("read more on how to use memEx") %>
|
||||
{gettext("read more on how to use memEx")}
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
@ -41,34 +41,43 @@
|
||||
<hr class="hr" />
|
||||
|
||||
<ul class="flex flex-col space-y-4">
|
||||
<h2 class="title text-primary-400 text-lg">
|
||||
<%= gettext("features") %>
|
||||
<h2 class="text-lg title text-primary-400">
|
||||
{gettext("features")}
|
||||
</h2>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("multi-user:") %>
|
||||
{gettext("multi-user:")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("built with sharing and collaboration in mind") %>
|
||||
{gettext("built with sharing and collaboration in mind")}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("privacy:") %>
|
||||
{gettext("privacy:")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("privacy controls on a per-note, context or pipeline basis") %>
|
||||
{gettext("privacy controls on a per-note, context or pipeline basis")}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("convenient:") %>
|
||||
{gettext("convenient:")}
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("accessible from any internet-capable device") %>
|
||||
{gettext("accessible from any internet-capable device")}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
{gettext("backlinks:")}
|
||||
</b>
|
||||
<p>
|
||||
{gettext("view referencing items from the referenced item")}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
@ -76,46 +85,46 @@
|
||||
<hr class="hr" />
|
||||
|
||||
<ul class="flex flex-col justify-center space-y-4">
|
||||
<h2 class="title text-primary-400 text-lg">
|
||||
<%= gettext("instance information") %>
|
||||
<h2 class="text-lg title text-primary-400">
|
||||
{gettext("instance information")}
|
||||
</h2>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b>
|
||||
<%= gettext("admins:") %>
|
||||
{gettext("admins:")}
|
||||
</b>
|
||||
<p class="flex flex-col justify-center items-center space-y-2">
|
||||
<%= if @admins |> Enum.empty?() do %>
|
||||
<.link href={~p"/users/register"} class="link">
|
||||
<%= dgettext("prompts", "register to setup memEx") %>
|
||||
{dgettext("prompts", "register to setup memEx")}
|
||||
</.link>
|
||||
<% else %>
|
||||
<.link :for={%{email: email} <- @admins} class="link" href={"mailto:#{email}"}>
|
||||
<%= email %>
|
||||
{email}
|
||||
</.link>
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b><%= gettext("registration:") %></b>
|
||||
<b>{gettext("registration:")}</b>
|
||||
<p>
|
||||
<%= case Accounts.registration_mode() do
|
||||
{case Accounts.registration_mode() do
|
||||
:public -> gettext("public signups")
|
||||
:invite_only -> gettext("invite only")
|
||||
end %>
|
||||
end}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
<b><%= gettext("version:") %></b>
|
||||
<b>{gettext("version:")}</b>
|
||||
<.link
|
||||
href="https://gitea.bubbletea.dev/shibao/memEx/src/branch/stable/changelog.md"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p><%= @version %></p>
|
||||
<p>{@version}</p>
|
||||
<i class="fas fa-md fa-info-circle"></i>
|
||||
</.link>
|
||||
</li>
|
||||
@ -124,8 +133,8 @@
|
||||
<hr class="hr" />
|
||||
|
||||
<ul class="flex flex-col space-y-2">
|
||||
<h2 class="title text-primary-400 text-lg">
|
||||
<%= gettext("get involved") %>
|
||||
<h2 class="text-lg title text-primary-400">
|
||||
{gettext("get involved")}
|
||||
</h2>
|
||||
|
||||
<li class="flex flex-col justify-center items-center space-y-2">
|
||||
@ -135,7 +144,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p><%= gettext("view the source code") %></p>
|
||||
<p>{gettext("view the source code")}</p>
|
||||
<i class="fas fa-md fa-code"></i>
|
||||
</.link>
|
||||
</li>
|
||||
@ -146,7 +155,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p><%= gettext("help translate") %></p>
|
||||
<p>{gettext("help translate")}</p>
|
||||
<i class="fas fa-md fa-language"></i>
|
||||
</.link>
|
||||
</li>
|
||||
@ -157,7 +166,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p><%= gettext("report bugs or request features") %></p>
|
||||
<p>{gettext("report bugs or request features")}</p>
|
||||
<i class="fas fa-md fa-spider"></i>
|
||||
</.link>
|
||||
</li>
|
||||
|
@ -82,7 +82,7 @@ defmodule MemexWeb.InviteLive.FormComponent do
|
||||
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
||||
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
socket |> assign(changeset: changeset)
|
||||
socket |> assign(:changeset, changeset)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
|
@ -1,37 +1,53 @@
|
||||
<div>
|
||||
<h2 class="mb-8 text-center title text-xl text-primary-400">
|
||||
<%= @title %>
|
||||
<h2 class="mb-8 text-xl text-center title text-primary-400">
|
||||
{@title}
|
||||
</h2>
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
id="invite-form"
|
||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||
class="flex flex-col justify-center items-center space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-hook="CtrlEnter"
|
||||
>
|
||||
<div
|
||||
:if={@changeset.action && not @changeset.valid?()}
|
||||
class="invalid-feedback col-span-3 text-center"
|
||||
:if={@changeset.action && not @changeset.valid?}
|
||||
class="col-span-3 text-center invalid-feedback"
|
||||
>
|
||||
<%= changeset_errors(@changeset) %>
|
||||
{changeset_errors(@changeset)}
|
||||
</div>
|
||||
|
||||
<%= label(f, :name, gettext("name"), class: "title text-lg text-primary-400") %>
|
||||
<%= text_input(f, :name, class: "input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :name, "col-span-3") %>
|
||||
{label(f, :name, gettext("name"),
|
||||
class: "title text-lg text-primary-400",
|
||||
phx_debounce: 300
|
||||
)}
|
||||
{text_input(f, :name,
|
||||
class: "input input-primary col-span-2",
|
||||
phx_debounce: 300,
|
||||
required: true
|
||||
)}
|
||||
{error_tag(f, :name, "col-span-3")}
|
||||
|
||||
<%= label(f, :uses_left, gettext("uses left"), class: "title text-lg text-primary-400") %>
|
||||
<%= number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2") %>
|
||||
<%= error_tag(f, :uses_left, "col-span-3") %>
|
||||
<span class="col-span-3 text-primary-500 italic text-center">
|
||||
<%= gettext(~s/leave "uses left" blank to make invite unlimited/) %>
|
||||
{label(f, :uses_left, gettext("uses left"),
|
||||
class: "title text-lg text-primary-400",
|
||||
phx_debounce: 300
|
||||
)}
|
||||
{number_input(f, :uses_left,
|
||||
min: 0,
|
||||
class: "input input-primary col-span-2",
|
||||
phx_debounce: 300
|
||||
)}
|
||||
{error_tag(f, :uses_left, "col-span-3")}
|
||||
|
||||
<span class="col-span-3 italic text-center text-primary-500">
|
||||
{gettext(~s/leave "uses left" blank to make invite unlimited/)}
|
||||
</span>
|
||||
|
||||
<%= submit(dgettext("actions", "save"),
|
||||
{submit(dgettext("actions", "save"),
|
||||
class: "mx-auto btn btn-primary col-span-3",
|
||||
phx_disable_with: dgettext("prompts", "saving...")
|
||||
) %>
|
||||
)}
|
||||
</.form>
|
||||
</div>
|
||||
|
@ -20,15 +20,15 @@ defmodule MemexWeb.InviteLive.Index do
|
||||
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(page_title: gettext("edit invite"), invite: Invites.get_invite!(id, current_user))
|
||||
|> assign(invite: Invites.get_invite!(id, current_user), page_title: gettext("edit invite"))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket |> assign(page_title: gettext("new invite"), invite: %Invite{})
|
||||
socket |> assign(invite: %Invite{}, page_title: gettext("new invite"))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket |> assign(page_title: gettext("invites"), invite: nil)
|
||||
socket |> assign(invite: nil, page_title: gettext("invites"))
|
||||
end
|
||||
|
||||
@impl true
|
||||
@ -93,7 +93,7 @@ defmodule MemexWeb.InviteLive.Index do
|
||||
%{"id" => id},
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
) do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
now = DateTime.utc_now()
|
||||
|
||||
socket =
|
||||
Invites.get_invite!(id, current_user)
|
||||
@ -138,6 +138,6 @@ defmodule MemexWeb.InviteLive.Index do
|
||||
|
||||
use_counts = invites |> Invites.get_use_counts(current_user)
|
||||
users = all_users |> Map.get(:user, [])
|
||||
socket |> assign(invites: invites, use_counts: use_counts, admins: admins, users: users)
|
||||
socket |> assign(admins: admins, invites: invites, use_counts: use_counts, users: users)
|
||||
end
|
||||
end
|
||||
|
@ -1,15 +1,15 @@
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
|
||||
<h1 class="title text-xl title-primary-500">
|
||||
<%= gettext("invites") %>
|
||||
<div class="flex flex-col justify-center items-stretch mx-auto space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl title title-primary-500">
|
||||
{gettext("invites")}
|
||||
</h1>
|
||||
|
||||
<%= if @invites |> Enum.empty?() do %>
|
||||
<h1 class="title text-xl text-primary-400">
|
||||
<%= gettext("no invites 😔") %>
|
||||
<h1 class="text-xl text-center title text-primary-400">
|
||||
{gettext("no invites 😔")}
|
||||
</h1>
|
||||
|
||||
<.link patch={~p"/invites"} class="btn btn-primary">
|
||||
<%= dgettext("actions", "invite someone new!") %>
|
||||
<.link patch={~p"/invites/new"} class="ml-auto btn btn-primary">
|
||||
{dgettext("actions", "new invite")}
|
||||
</.link>
|
||||
<% end %>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
dgettext("actions", "copy invite link for %{invite_name}", invite_name: invite.name)
|
||||
}
|
||||
>
|
||||
<%= dgettext("actions", "copy") %>
|
||||
{dgettext("actions", "copy")}
|
||||
</button>
|
||||
</form>
|
||||
</:code_actions>
|
||||
@ -67,11 +67,11 @@
|
||||
phx-click={if invite.disabled_at, do: "enable_invite", else: "disable_invite"}
|
||||
phx-value-id={invite.id}
|
||||
>
|
||||
<%= if invite.disabled_at, do: gettext("enable"), else: gettext("disable") %>
|
||||
{if invite.disabled_at, do: gettext("enable"), else: gettext("disable")}
|
||||
</.link>
|
||||
|
||||
<.link
|
||||
:if={invite.disabled_at |> is_nil() and not (invite.uses_left |> is_nil())}
|
||||
:if={!invite.disabled_at and !!invite.uses_left}
|
||||
href="#"
|
||||
class="btn btn-secondary"
|
||||
phx-click="set_unlimited"
|
||||
@ -82,20 +82,20 @@
|
||||
)
|
||||
}
|
||||
>
|
||||
<%= gettext("set unlimited") %>
|
||||
{gettext("set unlimited")}
|
||||
</.link>
|
||||
</.invite_card>
|
||||
|
||||
<.link :if={@invites != []} patch={~p"/invites/new"} class="btn btn-primary ml-auto">
|
||||
<%= dgettext("actions", "create invite") %>
|
||||
<.link :if={@invites != []} patch={~p"/invites/new"} class="ml-auto btn btn-primary">
|
||||
{dgettext("actions", "create invite")}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<%= unless @admins |> Enum.empty?() do %>
|
||||
<hr class="hr" />
|
||||
|
||||
<h1 class="title text-xl text-primary-400">
|
||||
<%= gettext("admins") %>
|
||||
<h1 class="text-xl title text-primary-400">
|
||||
{gettext("admins")}
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-col justify-center items-stretch space-y-4">
|
||||
@ -122,8 +122,8 @@
|
||||
<%= unless @users |> Enum.empty?() do %>
|
||||
<hr class="hr" />
|
||||
|
||||
<h1 class="title text-xl text-primary-400">
|
||||
<%= gettext("users") %>
|
||||
<h1 class="text-xl title text-primary-400">
|
||||
{gettext("users")}
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-col justify-center items-stretch space-y-4">
|
||||
|
@ -68,7 +68,7 @@ defmodule MemexWeb.NoteLive.FormComponent do
|
||||
|> push_navigate(to: return_to)
|
||||
|
||||
{:error, %Changeset{} = changeset} ->
|
||||
assign(socket, changeset: changeset)
|
||||
assign(socket, :changeset, changeset)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
|
||||
<div class="flex flex-col justify-start items-stretch space-y-4 h-full">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
@ -6,45 +6,52 @@
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-debounce="300"
|
||||
phx-hook="CtrlEnter"
|
||||
class="flex flex-col justify-start items-stretch space-y-4"
|
||||
>
|
||||
<%= text_input(f, :slug,
|
||||
{text_input(f, :slug,
|
||||
aria_label: gettext("slug"),
|
||||
class: "input input-primary",
|
||||
phx_debounce: 300,
|
||||
phx_hook: "SanitizeTitles",
|
||||
placeholder: gettext("slug"),
|
||||
aria_label: gettext("slug")
|
||||
) %>
|
||||
<%= error_tag(f, :slug) %>
|
||||
required: true
|
||||
)}
|
||||
{error_tag(f, :slug)}
|
||||
|
||||
<%= textarea(f, :content,
|
||||
{textarea(f, :content,
|
||||
id: "note-form-content",
|
||||
class: "input input-primary h-64 min-h-64",
|
||||
phx_update: "ignore",
|
||||
placeholder: gettext("content"),
|
||||
aria_label: gettext("content")
|
||||
) %>
|
||||
<%= error_tag(f, :content) %>
|
||||
placeholder: gettext("use [note-slug] to link to a note"),
|
||||
aria_label: gettext("use [note-slug] to link to a note"),
|
||||
phx_debounce: 300
|
||||
)}
|
||||
{error_tag(f, :content)}
|
||||
|
||||
<%= text_input(f, :tags_string,
|
||||
id: "tags-input",
|
||||
{text_input(f, :tags_string,
|
||||
aria_label: gettext("tag1,tag2"),
|
||||
class: "input input-primary",
|
||||
placeholder: gettext("tag1,tag2"),
|
||||
aria_label: gettext("tag1,tag2")
|
||||
) %>
|
||||
<%= error_tag(f, :tags_string) %>
|
||||
id: "tags-input",
|
||||
phx_debounce: 300,
|
||||
phx_hook: "SanitizeTags",
|
||||
placeholder: gettext("tag1,tag2")
|
||||
)}
|
||||
{error_tag(f, :tags_string)}
|
||||
|
||||
<div class="flex justify-center items-stretch space-x-4">
|
||||
<%= select(f, :visibility, Ecto.Enum.values(Memex.Notes.Note, :visibility),
|
||||
{select(f, :visibility, Ecto.Enum.values(Memex.Notes.Note, :visibility),
|
||||
class: "grow input input-primary",
|
||||
prompt: gettext("select privacy"),
|
||||
aria_label: gettext("select privacy")
|
||||
) %>
|
||||
aria_label: gettext("select privacy"),
|
||||
phx_debounce: 300
|
||||
)}
|
||||
|
||||
<%= submit(dgettext("actions", "save"),
|
||||
{submit(dgettext("actions", "save"),
|
||||
phx_disable_with: gettext("saving..."),
|
||||
class: "mx-auto btn btn-primary"
|
||||
) %>
|
||||
)}
|
||||
</div>
|
||||
<%= error_tag(f, :visibility) %>
|
||||
{error_tag(f, :visibility)}
|
||||
</.form>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user