Compare commits
	
		
			42 Commits
		
	
	
		
			0.1.9
			...
			56e6eb3609
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 56e6eb3609 | |||
| c49140e7f5 | |||
| 1276635a3e | |||
| f00dc50215 | |||
| 35de8a6395 | |||
| 96e155a49a | |||
| c02fb06eb2 | |||
| a9d5649bef | |||
| 650d61e95f | |||
| 63d854ffbe | |||
| a1c846be33 | |||
| 1b9f212e66 | |||
| 7805ddc270 | |||
| c1455bccad | |||
| dd956be93f | |||
| 04361a5838 | |||
| cb049cb178 | |||
| 5a41d8b3e7 | |||
| 64320dbdae | |||
| a03f8ebb8a | |||
| 4e17739b4d | |||
| d495db0bb3 | |||
| bf91eadd8e | |||
| 7d16cec5db | |||
| 50b7f8f9b6 | |||
| a0d3fb09f3 | |||
| 2d0f6eefab | |||
| d33da32b2f | |||
| 468489f872 | |||
| 5dedb4668b | |||
| 0d4deb6805 | |||
| 8f288afeb9 | |||
| aa5a1f30f9 | |||
| daa50039a7 | |||
| 98bb99881d | |||
| 56956f37fb | |||
| a0b93d0f46 | |||
| 066587f839 | |||
| 5cde99de90 | |||
| cc29f875fa | |||
| 5f2e69abbd | |||
| 8ef5147078 | 
@@ -17,7 +17,7 @@ steps:
 | 
			
		||||
      - .mix
 | 
			
		||||
 | 
			
		||||
- name: test
 | 
			
		||||
  image: elixir:1.14.1-alpine
 | 
			
		||||
  image: elixir:1.14.4-alpine
 | 
			
		||||
  environment:
 | 
			
		||||
    TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
 | 
			
		||||
    HOST: testing.example.tld
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
[
 | 
			
		||||
  import_deps: [:ecto, :phoenix],
 | 
			
		||||
  inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
 | 
			
		||||
  import_deps: [:ecto, :ecto_sql, :phoenix],
 | 
			
		||||
  inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
 | 
			
		||||
  subdirectories: ["priv/*/migrations"],
 | 
			
		||||
  plugins: [Phoenix.LiveView.HTMLFormatter]
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
elixir 1.14.1-otp-25
 | 
			
		||||
erlang 25.1.2
 | 
			
		||||
nodejs 18.9.1
 | 
			
		||||
elixir 1.14.4-otp-25
 | 
			
		||||
erlang 25.3
 | 
			
		||||
nodejs 18.15.0
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM elixir:1.14.1-alpine AS build
 | 
			
		||||
FROM elixir:1.14.4-alpine AS build
 | 
			
		||||
 | 
			
		||||
# install build dependencies
 | 
			
		||||
RUN apk add --no-cache build-base npm git python3
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.invalid-feedback {
 | 
			
		||||
  color: #a94442;
 | 
			
		||||
  color: #f36c69;
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin: -1rem 0 2rem;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,8 +45,8 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn-alert {
 | 
			
		||||
    @apply bg-red-700 active:bg-red-900;
 | 
			
		||||
    @apply border-red-700 active:border-red-900;
 | 
			
		||||
    @apply bg-red-800 active:bg-red-900;
 | 
			
		||||
    @apply border-red-800 active:border-red-900;
 | 
			
		||||
    @apply text-primary-300;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,26 +26,18 @@ import 'phoenix_html'
 | 
			
		||||
import { Socket } from 'phoenix'
 | 
			
		||||
import { LiveSocket } from 'phoenix_live_view'
 | 
			
		||||
import topbar from 'topbar'
 | 
			
		||||
import Date from './date'
 | 
			
		||||
import DateTime from './datetime'
 | 
			
		||||
import MaintainAttrs from './maintain_attrs'
 | 
			
		||||
import Alpine from 'alpinejs'
 | 
			
		||||
 | 
			
		||||
const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
 | 
			
		||||
let csrfToken
 | 
			
		||||
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
 | 
			
		||||
const liveSocket = new LiveSocket('/live', Socket, {
 | 
			
		||||
  dom: {
 | 
			
		||||
    onBeforeElUpdated (from, to) {
 | 
			
		||||
      if (from._x_dataStack) { window.Alpine.clone(from, to) }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  params: { _csrf_token: csrfToken },
 | 
			
		||||
  hooks: { MaintainAttrs }
 | 
			
		||||
  hooks: { Date, DateTime, MaintainAttrs }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// alpine.js
 | 
			
		||||
window.Alpine = Alpine
 | 
			
		||||
Alpine.start()
 | 
			
		||||
 | 
			
		||||
// Show progress bar on live navigation and form submits
 | 
			
		||||
topbar.config({ barThickness: 1, barColors: { 0: '#fff' }, shadowColor: 'rgba(0, 0, 0, .3)' })
 | 
			
		||||
window.addEventListener('phx:page-loading-start', info => topbar.show())
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								assets/js/date.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								assets/js/date.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
export default {
 | 
			
		||||
  displayDate (el) {
 | 
			
		||||
    const date =
 | 
			
		||||
      Intl.DateTimeFormat([], { timeZone: 'Etc/UTC', dateStyle: 'short' })
 | 
			
		||||
        .format(new Date(el.dateTime))
 | 
			
		||||
 | 
			
		||||
    el.innerText = date
 | 
			
		||||
  },
 | 
			
		||||
  mounted () { this.displayDate(this.el) },
 | 
			
		||||
  updated () { this.displayDate(this.el) }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								assets/js/datetime.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								assets/js/datetime.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
export default {
 | 
			
		||||
  displayDateTime (el) {
 | 
			
		||||
    const date =
 | 
			
		||||
      Intl.DateTimeFormat([], { dateStyle: 'short', timeStyle: 'long' })
 | 
			
		||||
        .format(new Date(el.dateTime))
 | 
			
		||||
 | 
			
		||||
    el.innerText = date
 | 
			
		||||
  },
 | 
			
		||||
  mounted () { this.displayDateTime(this.el) },
 | 
			
		||||
  updated () { this.displayDateTime(this.el) }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15434
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15434
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,8 +3,8 @@
 | 
			
		||||
  "description": " ",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": "v18.9.1",
 | 
			
		||||
    "npm": "8.10.0"
 | 
			
		||||
    "node": "v18.15.0",
 | 
			
		||||
    "npm": "9.5.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "deploy": "NODE_ENV=production webpack --mode production",
 | 
			
		||||
@@ -13,35 +13,35 @@
 | 
			
		||||
    "test": "standard"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fontsource/nunito-sans": "^4.5.8",
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^6.1.1",
 | 
			
		||||
    "alpinejs": "^3.10.2",
 | 
			
		||||
    "@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": "^1.0.1"
 | 
			
		||||
    "topbar": "^2.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "^7.17.10",
 | 
			
		||||
    "@babel/preset-env": "^7.17.10",
 | 
			
		||||
    "autoprefixer": "^10.4.7",
 | 
			
		||||
    "babel-loader": "^8.2.5",
 | 
			
		||||
    "copy-webpack-plugin": "^10.2.4",
 | 
			
		||||
    "css-loader": "^6.7.1",
 | 
			
		||||
    "css-minimizer-webpack-plugin": "^3.4.1",
 | 
			
		||||
    "@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.6.0",
 | 
			
		||||
    "postcss": "^8.4.13",
 | 
			
		||||
    "postcss-import": "^14.1.0",
 | 
			
		||||
    "postcss-loader": "^6.2.1",
 | 
			
		||||
    "postcss-preset-env": "^7.5.0",
 | 
			
		||||
    "sass": "^1.56.0",
 | 
			
		||||
    "sass-loader": "^12.6.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.0.24",
 | 
			
		||||
    "terser-webpack-plugin": "^5.3.1",
 | 
			
		||||
    "webpack": "^5.72.0",
 | 
			
		||||
    "webpack-cli": "^4.9.2",
 | 
			
		||||
    "webpack-dev-server": "^4.9.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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ module.exports = (env, options) => {
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
 | 
			
		||||
          type: 'asset/resource',
 | 
			
		||||
          generator: { filename: 'fonts/[name][ext]' }
 | 
			
		||||
          generator: { filename: 'fonts/[name].[ext]' }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,3 +1,14 @@
 | 
			
		||||
# v0.1.11
 | 
			
		||||
- Update dependencies
 | 
			
		||||
 | 
			
		||||
# v0.1.10
 | 
			
		||||
- Improve accessibility
 | 
			
		||||
- Code quality improvements
 | 
			
		||||
- Fix dates displaying incorrectly
 | 
			
		||||
- Add links to readme for github mirror
 | 
			
		||||
- Add license (whoops)
 | 
			
		||||
- Display links in note/context/step contents
 | 
			
		||||
 | 
			
		||||
# v0.1.9
 | 
			
		||||
- Improve server log
 | 
			
		||||
- Style 大一點
 | 
			
		||||
 
 | 
			
		||||
@@ -59,8 +59,7 @@ config :memex, MemexWeb.Endpoint,
 | 
			
		||||
    patterns: [
 | 
			
		||||
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
 | 
			
		||||
      ~r"priv/gettext/.*(po)$",
 | 
			
		||||
      ~r"lib/memex_web/(live|views)/.*(ex)$",
 | 
			
		||||
      ~r"lib/memex_web/templates/.*(eex)$"
 | 
			
		||||
      ~r"lib/memex_web/*/.*(ex)$"
 | 
			
		||||
    ]
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										660
									
								
								lib/license.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								lib/license.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,660 @@
 | 
			
		||||
### GNU AFFERO GENERAL PUBLIC LICENSE
 | 
			
		||||
 | 
			
		||||
Version 3, 19 November 2007
 | 
			
		||||
 | 
			
		||||
Copyright (C) 2007 Free Software Foundation, Inc.
 | 
			
		||||
<https://fsf.org/>
 | 
			
		||||
 | 
			
		||||
Everyone is permitted to copy and distribute verbatim copies of this
 | 
			
		||||
license document, but changing it is not allowed.
 | 
			
		||||
 | 
			
		||||
### Preamble
 | 
			
		||||
 | 
			
		||||
The GNU Affero General Public License is a free, copyleft license for
 | 
			
		||||
software and other kinds of works, specifically designed to ensure
 | 
			
		||||
cooperation with the community in the case of network server software.
 | 
			
		||||
 | 
			
		||||
The licenses for most software and other practical works are designed
 | 
			
		||||
to take away your freedom to share and change the works. By contrast,
 | 
			
		||||
our General Public Licenses are intended to guarantee your freedom to
 | 
			
		||||
share and change all versions of a program--to make sure it remains
 | 
			
		||||
free software for all its users.
 | 
			
		||||
 | 
			
		||||
When we speak of free software, we are referring to freedom, not
 | 
			
		||||
price. Our General Public Licenses are designed to make sure that you
 | 
			
		||||
have the freedom to distribute copies of free software (and charge for
 | 
			
		||||
them if you wish), that you receive source code or can get it if you
 | 
			
		||||
want it, that you can change the software or use pieces of it in new
 | 
			
		||||
free programs, and that you know you can do these things.
 | 
			
		||||
 | 
			
		||||
Developers that use our General Public Licenses protect your rights
 | 
			
		||||
with two steps: (1) assert copyright on the software, and (2) offer
 | 
			
		||||
you this License which gives you legal permission to copy, distribute
 | 
			
		||||
and/or modify the software.
 | 
			
		||||
 | 
			
		||||
A secondary benefit of defending all users' freedom is that
 | 
			
		||||
improvements made in alternate versions of the program, if they
 | 
			
		||||
receive widespread use, become available for other developers to
 | 
			
		||||
incorporate. Many developers of free software are heartened and
 | 
			
		||||
encouraged by the resulting cooperation. However, in the case of
 | 
			
		||||
software used on network servers, this result may fail to come about.
 | 
			
		||||
The GNU General Public License permits making a modified version and
 | 
			
		||||
letting the public access it on a server without ever releasing its
 | 
			
		||||
source code to the public.
 | 
			
		||||
 | 
			
		||||
The GNU Affero General Public License is designed specifically to
 | 
			
		||||
ensure that, in such cases, the modified source code becomes available
 | 
			
		||||
to the community. It requires the operator of a network server to
 | 
			
		||||
provide the source code of the modified version running there to the
 | 
			
		||||
users of that server. Therefore, public use of a modified version, on
 | 
			
		||||
a publicly accessible server, gives the public access to the source
 | 
			
		||||
code of the modified version.
 | 
			
		||||
 | 
			
		||||
An older license, called the Affero General Public License and
 | 
			
		||||
published by Affero, was designed to accomplish similar goals. This is
 | 
			
		||||
a different license, not a version of the Affero GPL, but Affero has
 | 
			
		||||
released a new version of the Affero GPL which permits relicensing
 | 
			
		||||
under this license.
 | 
			
		||||
 | 
			
		||||
The precise terms and conditions for copying, distribution and
 | 
			
		||||
modification follow.
 | 
			
		||||
 | 
			
		||||
### TERMS AND CONDITIONS
 | 
			
		||||
 | 
			
		||||
#### 0. Definitions.
 | 
			
		||||
 | 
			
		||||
"This License" refers to version 3 of the GNU Affero General Public
 | 
			
		||||
License.
 | 
			
		||||
 | 
			
		||||
"Copyright" also means copyright-like laws that apply to other kinds
 | 
			
		||||
of works, such as semiconductor masks.
 | 
			
		||||
 | 
			
		||||
"The Program" refers to any copyrightable work licensed under this
 | 
			
		||||
License. Each licensee is addressed as "you". "Licensees" and
 | 
			
		||||
"recipients" may be individuals or organizations.
 | 
			
		||||
 | 
			
		||||
To "modify" a work means to copy from or adapt all or part of the work
 | 
			
		||||
in a fashion requiring copyright permission, other than the making of
 | 
			
		||||
an exact copy. The resulting work is called a "modified version" of
 | 
			
		||||
the earlier work or a work "based on" the earlier work.
 | 
			
		||||
 | 
			
		||||
A "covered work" means either the unmodified Program or a work based
 | 
			
		||||
on the Program.
 | 
			
		||||
 | 
			
		||||
To "propagate" a work means to do anything with it that, without
 | 
			
		||||
permission, would make you directly or secondarily liable for
 | 
			
		||||
infringement under applicable copyright law, except executing it on a
 | 
			
		||||
computer or modifying a private copy. Propagation includes copying,
 | 
			
		||||
distribution (with or without modification), making available to the
 | 
			
		||||
public, and in some countries other activities as well.
 | 
			
		||||
 | 
			
		||||
To "convey" a work means any kind of propagation that enables other
 | 
			
		||||
parties to make or receive copies. Mere interaction with a user
 | 
			
		||||
through a computer network, with no transfer of a copy, is not
 | 
			
		||||
conveying.
 | 
			
		||||
 | 
			
		||||
An interactive user interface displays "Appropriate Legal Notices" to
 | 
			
		||||
the extent that it includes a convenient and prominently visible
 | 
			
		||||
feature that (1) displays an appropriate copyright notice, and (2)
 | 
			
		||||
tells the user that there is no warranty for the work (except to the
 | 
			
		||||
extent that warranties are provided), that licensees may convey the
 | 
			
		||||
work under this License, and how to view a copy of this License. If
 | 
			
		||||
the interface presents a list of user commands or options, such as a
 | 
			
		||||
menu, a prominent item in the list meets this criterion.
 | 
			
		||||
 | 
			
		||||
#### 1. Source Code.
 | 
			
		||||
 | 
			
		||||
The "source code" for a work means the preferred form of the work for
 | 
			
		||||
making modifications to it. "Object code" means any non-source form of
 | 
			
		||||
a work.
 | 
			
		||||
 | 
			
		||||
A "Standard Interface" means an interface that either is an official
 | 
			
		||||
standard defined by a recognized standards body, or, in the case of
 | 
			
		||||
interfaces specified for a particular programming language, one that
 | 
			
		||||
is widely used among developers working in that language.
 | 
			
		||||
 | 
			
		||||
The "System Libraries" of an executable work include anything, other
 | 
			
		||||
than the work as a whole, that (a) is included in the normal form of
 | 
			
		||||
packaging a Major Component, but which is not part of that Major
 | 
			
		||||
Component, and (b) serves only to enable use of the work with that
 | 
			
		||||
Major Component, or to implement a Standard Interface for which an
 | 
			
		||||
implementation is available to the public in source code form. A
 | 
			
		||||
"Major Component", in this context, means a major essential component
 | 
			
		||||
(kernel, window system, and so on) of the specific operating system
 | 
			
		||||
(if any) on which the executable work runs, or a compiler used to
 | 
			
		||||
produce the work, or an object code interpreter used to run it.
 | 
			
		||||
 | 
			
		||||
The "Corresponding Source" for a work in object code form means all
 | 
			
		||||
the source code needed to generate, install, and (for an executable
 | 
			
		||||
work) run the object code and to modify the work, including scripts to
 | 
			
		||||
control those activities. However, it does not include the work's
 | 
			
		||||
System Libraries, or general-purpose tools or generally available free
 | 
			
		||||
programs which are used unmodified in performing those activities but
 | 
			
		||||
which are not part of the work. For example, Corresponding Source
 | 
			
		||||
includes interface definition files associated with source files for
 | 
			
		||||
the work, and the source code for shared libraries and dynamically
 | 
			
		||||
linked subprograms that the work is specifically designed to require,
 | 
			
		||||
such as by intimate data communication or control flow between those
 | 
			
		||||
subprograms and other parts of the work.
 | 
			
		||||
 | 
			
		||||
The Corresponding Source need not include anything that users can
 | 
			
		||||
regenerate automatically from other parts of the Corresponding Source.
 | 
			
		||||
 | 
			
		||||
The Corresponding Source for a work in source code form is that same
 | 
			
		||||
work.
 | 
			
		||||
 | 
			
		||||
#### 2. Basic Permissions.
 | 
			
		||||
 | 
			
		||||
All rights granted under this License are granted for the term of
 | 
			
		||||
copyright on the Program, and are irrevocable provided the stated
 | 
			
		||||
conditions are met. This License explicitly affirms your unlimited
 | 
			
		||||
permission to run the unmodified Program. The output from running a
 | 
			
		||||
covered work is covered by this License only if the output, given its
 | 
			
		||||
content, constitutes a covered work. This License acknowledges your
 | 
			
		||||
rights of fair use or other equivalent, as provided by copyright law.
 | 
			
		||||
 | 
			
		||||
You may make, run and propagate covered works that you do not convey,
 | 
			
		||||
without conditions so long as your license otherwise remains in force.
 | 
			
		||||
You may convey covered works to others for the sole purpose of having
 | 
			
		||||
them make modifications exclusively for you, or provide you with
 | 
			
		||||
facilities for running those works, provided that you comply with the
 | 
			
		||||
terms of this License in conveying all material for which you do not
 | 
			
		||||
control copyright. Those thus making or running the covered works for
 | 
			
		||||
you must do so exclusively on your behalf, under your direction and
 | 
			
		||||
control, on terms that prohibit them from making any copies of your
 | 
			
		||||
copyrighted material outside their relationship with you.
 | 
			
		||||
 | 
			
		||||
Conveying under any other circumstances is permitted solely under the
 | 
			
		||||
conditions stated below. Sublicensing is not allowed; section 10 makes
 | 
			
		||||
it unnecessary.
 | 
			
		||||
 | 
			
		||||
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
 | 
			
		||||
 | 
			
		||||
No covered work shall be deemed part of an effective technological
 | 
			
		||||
measure under any applicable law fulfilling obligations under article
 | 
			
		||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
 | 
			
		||||
similar laws prohibiting or restricting circumvention of such
 | 
			
		||||
measures.
 | 
			
		||||
 | 
			
		||||
When you convey a covered work, you waive any legal power to forbid
 | 
			
		||||
circumvention of technological measures to the extent such
 | 
			
		||||
circumvention is effected by exercising rights under this License with
 | 
			
		||||
respect to the covered work, and you disclaim any intention to limit
 | 
			
		||||
operation or modification of the work as a means of enforcing, against
 | 
			
		||||
the work's users, your or third parties' legal rights to forbid
 | 
			
		||||
circumvention of technological measures.
 | 
			
		||||
 | 
			
		||||
#### 4. Conveying Verbatim Copies.
 | 
			
		||||
 | 
			
		||||
You may convey verbatim copies of the Program's source code as you
 | 
			
		||||
receive it, in any medium, provided that you conspicuously and
 | 
			
		||||
appropriately publish on each copy an appropriate copyright notice;
 | 
			
		||||
keep intact all notices stating that this License and any
 | 
			
		||||
non-permissive terms added in accord with section 7 apply to the code;
 | 
			
		||||
keep intact all notices of the absence of any warranty; and give all
 | 
			
		||||
recipients a copy of this License along with the Program.
 | 
			
		||||
 | 
			
		||||
You may charge any price or no price for each copy that you convey,
 | 
			
		||||
and you may offer support or warranty protection for a fee.
 | 
			
		||||
 | 
			
		||||
#### 5. Conveying Modified Source Versions.
 | 
			
		||||
 | 
			
		||||
You may convey a work based on the Program, or the modifications to
 | 
			
		||||
produce it from the Program, in the form of source code under the
 | 
			
		||||
terms of section 4, provided that you also meet all of these
 | 
			
		||||
conditions:
 | 
			
		||||
 | 
			
		||||
-   a) The work must carry prominent notices stating that you modified
 | 
			
		||||
    it, and giving a relevant date.
 | 
			
		||||
-   b) The work must carry prominent notices stating that it is
 | 
			
		||||
    released under this License and any conditions added under
 | 
			
		||||
    section 7. This requirement modifies the requirement in section 4
 | 
			
		||||
    to "keep intact all notices".
 | 
			
		||||
-   c) You must license the entire work, as a whole, under this
 | 
			
		||||
    License to anyone who comes into possession of a copy. This
 | 
			
		||||
    License will therefore apply, along with any applicable section 7
 | 
			
		||||
    additional terms, to the whole of the work, and all its parts,
 | 
			
		||||
    regardless of how they are packaged. This License gives no
 | 
			
		||||
    permission to license the work in any other way, but it does not
 | 
			
		||||
    invalidate such permission if you have separately received it.
 | 
			
		||||
-   d) If the work has interactive user interfaces, each must display
 | 
			
		||||
    Appropriate Legal Notices; however, if the Program has interactive
 | 
			
		||||
    interfaces that do not display Appropriate Legal Notices, your
 | 
			
		||||
    work need not make them do so.
 | 
			
		||||
 | 
			
		||||
A compilation of a covered work with other separate and independent
 | 
			
		||||
works, which are not by their nature extensions of the covered work,
 | 
			
		||||
and which are not combined with it such as to form a larger program,
 | 
			
		||||
in or on a volume of a storage or distribution medium, is called an
 | 
			
		||||
"aggregate" if the compilation and its resulting copyright are not
 | 
			
		||||
used to limit the access or legal rights of the compilation's users
 | 
			
		||||
beyond what the individual works permit. Inclusion of a covered work
 | 
			
		||||
in an aggregate does not cause this License to apply to the other
 | 
			
		||||
parts of the aggregate.
 | 
			
		||||
 | 
			
		||||
#### 6. Conveying Non-Source Forms.
 | 
			
		||||
 | 
			
		||||
You may convey a covered work in object code form under the terms of
 | 
			
		||||
sections 4 and 5, provided that you also convey the machine-readable
 | 
			
		||||
Corresponding Source under the terms of this License, in one of these
 | 
			
		||||
ways:
 | 
			
		||||
 | 
			
		||||
-   a) Convey the object code in, or embodied in, a physical product
 | 
			
		||||
    (including a physical distribution medium), accompanied by the
 | 
			
		||||
    Corresponding Source fixed on a durable physical medium
 | 
			
		||||
    customarily used for software interchange.
 | 
			
		||||
-   b) Convey the object code in, or embodied in, a physical product
 | 
			
		||||
    (including a physical distribution medium), accompanied by a
 | 
			
		||||
    written offer, valid for at least three years and valid for as
 | 
			
		||||
    long as you offer spare parts or customer support for that product
 | 
			
		||||
    model, to give anyone who possesses the object code either (1) a
 | 
			
		||||
    copy of the Corresponding Source for all the software in the
 | 
			
		||||
    product that is covered by this License, on a durable physical
 | 
			
		||||
    medium customarily used for software interchange, for a price no
 | 
			
		||||
    more than your reasonable cost of physically performing this
 | 
			
		||||
    conveying of source, or (2) access to copy the Corresponding
 | 
			
		||||
    Source from a network server at no charge.
 | 
			
		||||
-   c) Convey individual copies of the object code with a copy of the
 | 
			
		||||
    written offer to provide the Corresponding Source. This
 | 
			
		||||
    alternative is allowed only occasionally and noncommercially, and
 | 
			
		||||
    only if you received the object code with such an offer, in accord
 | 
			
		||||
    with subsection 6b.
 | 
			
		||||
-   d) Convey the object code by offering access from a designated
 | 
			
		||||
    place (gratis or for a charge), and offer equivalent access to the
 | 
			
		||||
    Corresponding Source in the same way through the same place at no
 | 
			
		||||
    further charge. You need not require recipients to copy the
 | 
			
		||||
    Corresponding Source along with the object code. If the place to
 | 
			
		||||
    copy the object code is a network server, the Corresponding Source
 | 
			
		||||
    may be on a different server (operated by you or a third party)
 | 
			
		||||
    that supports equivalent copying facilities, provided you maintain
 | 
			
		||||
    clear directions next to the object code saying where to find the
 | 
			
		||||
    Corresponding Source. Regardless of what server hosts the
 | 
			
		||||
    Corresponding Source, you remain obligated to ensure that it is
 | 
			
		||||
    available for as long as needed to satisfy these requirements.
 | 
			
		||||
-   e) Convey the object code using peer-to-peer transmission,
 | 
			
		||||
    provided you inform other peers where the object code and
 | 
			
		||||
    Corresponding Source of the work are being offered to the general
 | 
			
		||||
    public at no charge under subsection 6d.
 | 
			
		||||
 | 
			
		||||
A separable portion of the object code, whose source code is excluded
 | 
			
		||||
from the Corresponding Source as a System Library, need not be
 | 
			
		||||
included in conveying the object code work.
 | 
			
		||||
 | 
			
		||||
A "User Product" is either (1) a "consumer product", which means any
 | 
			
		||||
tangible personal property which is normally used for personal,
 | 
			
		||||
family, or household purposes, or (2) anything designed or sold for
 | 
			
		||||
incorporation into a dwelling. In determining whether a product is a
 | 
			
		||||
consumer product, doubtful cases shall be resolved in favor of
 | 
			
		||||
coverage. For a particular product received by a particular user,
 | 
			
		||||
"normally used" refers to a typical or common use of that class of
 | 
			
		||||
product, regardless of the status of the particular user or of the way
 | 
			
		||||
in which the particular user actually uses, or expects or is expected
 | 
			
		||||
to use, the product. A product is a consumer product regardless of
 | 
			
		||||
whether the product has substantial commercial, industrial or
 | 
			
		||||
non-consumer uses, unless such uses represent the only significant
 | 
			
		||||
mode of use of the product.
 | 
			
		||||
 | 
			
		||||
"Installation Information" for a User Product means any methods,
 | 
			
		||||
procedures, authorization keys, or other information required to
 | 
			
		||||
install and execute modified versions of a covered work in that User
 | 
			
		||||
Product from a modified version of its Corresponding Source. The
 | 
			
		||||
information must suffice to ensure that the continued functioning of
 | 
			
		||||
the modified object code is in no case prevented or interfered with
 | 
			
		||||
solely because modification has been made.
 | 
			
		||||
 | 
			
		||||
If you convey an object code work under this section in, or with, or
 | 
			
		||||
specifically for use in, a User Product, and the conveying occurs as
 | 
			
		||||
part of a transaction in which the right of possession and use of the
 | 
			
		||||
User Product is transferred to the recipient in perpetuity or for a
 | 
			
		||||
fixed term (regardless of how the transaction is characterized), the
 | 
			
		||||
Corresponding Source conveyed under this section must be accompanied
 | 
			
		||||
by the Installation Information. But this requirement does not apply
 | 
			
		||||
if neither you nor any third party retains the ability to install
 | 
			
		||||
modified object code on the User Product (for example, the work has
 | 
			
		||||
been installed in ROM).
 | 
			
		||||
 | 
			
		||||
The requirement to provide Installation Information does not include a
 | 
			
		||||
requirement to continue to provide support service, warranty, or
 | 
			
		||||
updates for a work that has been modified or installed by the
 | 
			
		||||
recipient, or for the User Product in which it has been modified or
 | 
			
		||||
installed. Access to a network may be denied when the modification
 | 
			
		||||
itself materially and adversely affects the operation of the network
 | 
			
		||||
or violates the rules and protocols for communication across the
 | 
			
		||||
network.
 | 
			
		||||
 | 
			
		||||
Corresponding Source conveyed, and Installation Information provided,
 | 
			
		||||
in accord with this section must be in a format that is publicly
 | 
			
		||||
documented (and with an implementation available to the public in
 | 
			
		||||
source code form), and must require no special password or key for
 | 
			
		||||
unpacking, reading or copying.
 | 
			
		||||
 | 
			
		||||
#### 7. Additional Terms.
 | 
			
		||||
 | 
			
		||||
"Additional permissions" are terms that supplement the terms of this
 | 
			
		||||
License by making exceptions from one or more of its conditions.
 | 
			
		||||
Additional permissions that are applicable to the entire Program shall
 | 
			
		||||
be treated as though they were included in this License, to the extent
 | 
			
		||||
that they are valid under applicable law. If additional permissions
 | 
			
		||||
apply only to part of the Program, that part may be used separately
 | 
			
		||||
under those permissions, but the entire Program remains governed by
 | 
			
		||||
this License without regard to the additional permissions.
 | 
			
		||||
 | 
			
		||||
When you convey a copy of a covered work, you may at your option
 | 
			
		||||
remove any additional permissions from that copy, or from any part of
 | 
			
		||||
it. (Additional permissions may be written to require their own
 | 
			
		||||
removal in certain cases when you modify the work.) You may place
 | 
			
		||||
additional permissions on material, added by you to a covered work,
 | 
			
		||||
for which you have or can give appropriate copyright permission.
 | 
			
		||||
 | 
			
		||||
Notwithstanding any other provision of this License, for material you
 | 
			
		||||
add to a covered work, you may (if authorized by the copyright holders
 | 
			
		||||
of that material) supplement the terms of this License with terms:
 | 
			
		||||
 | 
			
		||||
-   a) Disclaiming warranty or limiting liability differently from the
 | 
			
		||||
    terms of sections 15 and 16 of this License; or
 | 
			
		||||
-   b) Requiring preservation of specified reasonable legal notices or
 | 
			
		||||
    author attributions in that material or in the Appropriate Legal
 | 
			
		||||
    Notices displayed by works containing it; or
 | 
			
		||||
-   c) Prohibiting misrepresentation of the origin of that material,
 | 
			
		||||
    or requiring that modified versions of such material be marked in
 | 
			
		||||
    reasonable ways as different from the original version; or
 | 
			
		||||
-   d) Limiting the use for publicity purposes of names of licensors
 | 
			
		||||
    or authors of the material; or
 | 
			
		||||
-   e) Declining to grant rights under trademark law for use of some
 | 
			
		||||
    trade names, trademarks, or service marks; or
 | 
			
		||||
-   f) Requiring indemnification of licensors and authors of that
 | 
			
		||||
    material by anyone who conveys the material (or modified versions
 | 
			
		||||
    of it) with contractual assumptions of liability to the recipient,
 | 
			
		||||
    for any liability that these contractual assumptions directly
 | 
			
		||||
    impose on those licensors and authors.
 | 
			
		||||
 | 
			
		||||
All other non-permissive additional terms are considered "further
 | 
			
		||||
restrictions" within the meaning of section 10. If the Program as you
 | 
			
		||||
received it, or any part of it, contains a notice stating that it is
 | 
			
		||||
governed by this License along with a term that is a further
 | 
			
		||||
restriction, you may remove that term. If a license document contains
 | 
			
		||||
a further restriction but permits relicensing or conveying under this
 | 
			
		||||
License, you may add to a covered work material governed by the terms
 | 
			
		||||
of that license document, provided that the further restriction does
 | 
			
		||||
not survive such relicensing or conveying.
 | 
			
		||||
 | 
			
		||||
If you add terms to a covered work in accord with this section, you
 | 
			
		||||
must place, in the relevant source files, a statement of the
 | 
			
		||||
additional terms that apply to those files, or a notice indicating
 | 
			
		||||
where to find the applicable terms.
 | 
			
		||||
 | 
			
		||||
Additional terms, permissive or non-permissive, may be stated in the
 | 
			
		||||
form of a separately written license, or stated as exceptions; the
 | 
			
		||||
above requirements apply either way.
 | 
			
		||||
 | 
			
		||||
#### 8. Termination.
 | 
			
		||||
 | 
			
		||||
You may not propagate or modify a covered work except as expressly
 | 
			
		||||
provided under this License. Any attempt otherwise to propagate or
 | 
			
		||||
modify it is void, and will automatically terminate your rights under
 | 
			
		||||
this License (including any patent licenses granted under the third
 | 
			
		||||
paragraph of section 11).
 | 
			
		||||
 | 
			
		||||
However, if you cease all violation of this License, then your license
 | 
			
		||||
from a particular copyright holder is reinstated (a) provisionally,
 | 
			
		||||
unless and until the copyright holder explicitly and finally
 | 
			
		||||
terminates your license, and (b) permanently, if the copyright holder
 | 
			
		||||
fails to notify you of the violation by some reasonable means prior to
 | 
			
		||||
60 days after the cessation.
 | 
			
		||||
 | 
			
		||||
Moreover, your license from a particular copyright holder is
 | 
			
		||||
reinstated permanently if the copyright holder notifies you of the
 | 
			
		||||
violation by some reasonable means, this is the first time you have
 | 
			
		||||
received notice of violation of this License (for any work) from that
 | 
			
		||||
copyright holder, and you cure the violation prior to 30 days after
 | 
			
		||||
your receipt of the notice.
 | 
			
		||||
 | 
			
		||||
Termination of your rights under this section does not terminate the
 | 
			
		||||
licenses of parties who have received copies or rights from you under
 | 
			
		||||
this License. If your rights have been terminated and not permanently
 | 
			
		||||
reinstated, you do not qualify to receive new licenses for the same
 | 
			
		||||
material under section 10.
 | 
			
		||||
 | 
			
		||||
#### 9. Acceptance Not Required for Having Copies.
 | 
			
		||||
 | 
			
		||||
You are not required to accept this License in order to receive or run
 | 
			
		||||
a copy of the Program. Ancillary propagation of a covered work
 | 
			
		||||
occurring solely as a consequence of using peer-to-peer transmission
 | 
			
		||||
to receive a copy likewise does not require acceptance. However,
 | 
			
		||||
nothing other than this License grants you permission to propagate or
 | 
			
		||||
modify any covered work. These actions infringe copyright if you do
 | 
			
		||||
not accept this License. Therefore, by modifying or propagating a
 | 
			
		||||
covered work, you indicate your acceptance of this License to do so.
 | 
			
		||||
 | 
			
		||||
#### 10. Automatic Licensing of Downstream Recipients.
 | 
			
		||||
 | 
			
		||||
Each time you convey a covered work, the recipient automatically
 | 
			
		||||
receives a license from the original licensors, to run, modify and
 | 
			
		||||
propagate that work, subject to this License. You are not responsible
 | 
			
		||||
for enforcing compliance by third parties with this License.
 | 
			
		||||
 | 
			
		||||
An "entity transaction" is a transaction transferring control of an
 | 
			
		||||
organization, or substantially all assets of one, or subdividing an
 | 
			
		||||
organization, or merging organizations. If propagation of a covered
 | 
			
		||||
work results from an entity transaction, each party to that
 | 
			
		||||
transaction who receives a copy of the work also receives whatever
 | 
			
		||||
licenses to the work the party's predecessor in interest had or could
 | 
			
		||||
give under the previous paragraph, plus a right to possession of the
 | 
			
		||||
Corresponding Source of the work from the predecessor in interest, if
 | 
			
		||||
the predecessor has it or can get it with reasonable efforts.
 | 
			
		||||
 | 
			
		||||
You may not impose any further restrictions on the exercise of the
 | 
			
		||||
rights granted or affirmed under this License. For example, you may
 | 
			
		||||
not impose a license fee, royalty, or other charge for exercise of
 | 
			
		||||
rights granted under this License, and you may not initiate litigation
 | 
			
		||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
 | 
			
		||||
any patent claim is infringed by making, using, selling, offering for
 | 
			
		||||
sale, or importing the Program or any portion of it.
 | 
			
		||||
 | 
			
		||||
#### 11. Patents.
 | 
			
		||||
 | 
			
		||||
A "contributor" is a copyright holder who authorizes use under this
 | 
			
		||||
License of the Program or a work on which the Program is based. The
 | 
			
		||||
work thus licensed is called the contributor's "contributor version".
 | 
			
		||||
 | 
			
		||||
A contributor's "essential patent claims" are all patent claims owned
 | 
			
		||||
or controlled by the contributor, whether already acquired or
 | 
			
		||||
hereafter acquired, that would be infringed by some manner, permitted
 | 
			
		||||
by this License, of making, using, or selling its contributor version,
 | 
			
		||||
but do not include claims that would be infringed only as a
 | 
			
		||||
consequence of further modification of the contributor version. For
 | 
			
		||||
purposes of this definition, "control" includes the right to grant
 | 
			
		||||
patent sublicenses in a manner consistent with the requirements of
 | 
			
		||||
this License.
 | 
			
		||||
 | 
			
		||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
 | 
			
		||||
patent license under the contributor's essential patent claims, to
 | 
			
		||||
make, use, sell, offer for sale, import and otherwise run, modify and
 | 
			
		||||
propagate the contents of its contributor version.
 | 
			
		||||
 | 
			
		||||
In the following three paragraphs, a "patent license" is any express
 | 
			
		||||
agreement or commitment, however denominated, not to enforce a patent
 | 
			
		||||
(such as an express permission to practice a patent or covenant not to
 | 
			
		||||
sue for patent infringement). To "grant" such a patent license to a
 | 
			
		||||
party means to make such an agreement or commitment not to enforce a
 | 
			
		||||
patent against the party.
 | 
			
		||||
 | 
			
		||||
If you convey a covered work, knowingly relying on a patent license,
 | 
			
		||||
and the Corresponding Source of the work is not available for anyone
 | 
			
		||||
to copy, free of charge and under the terms of this License, through a
 | 
			
		||||
publicly available network server or other readily accessible means,
 | 
			
		||||
then you must either (1) cause the Corresponding Source to be so
 | 
			
		||||
available, or (2) arrange to deprive yourself of the benefit of the
 | 
			
		||||
patent license for this particular work, or (3) arrange, in a manner
 | 
			
		||||
consistent with the requirements of this License, to extend the patent
 | 
			
		||||
license to downstream recipients. "Knowingly relying" means you have
 | 
			
		||||
actual knowledge that, but for the patent license, your conveying the
 | 
			
		||||
covered work in a country, or your recipient's use of the covered work
 | 
			
		||||
in a country, would infringe one or more identifiable patents in that
 | 
			
		||||
country that you have reason to believe are valid.
 | 
			
		||||
 | 
			
		||||
If, pursuant to or in connection with a single transaction or
 | 
			
		||||
arrangement, you convey, or propagate by procuring conveyance of, a
 | 
			
		||||
covered work, and grant a patent license to some of the parties
 | 
			
		||||
receiving the covered work authorizing them to use, propagate, modify
 | 
			
		||||
or convey a specific copy of the covered work, then the patent license
 | 
			
		||||
you grant is automatically extended to all recipients of the covered
 | 
			
		||||
work and works based on it.
 | 
			
		||||
 | 
			
		||||
A patent license is "discriminatory" if it does not include within the
 | 
			
		||||
scope of its coverage, prohibits the exercise of, or is conditioned on
 | 
			
		||||
the non-exercise of one or more of the rights that are specifically
 | 
			
		||||
granted under this License. You may not convey a covered work if you
 | 
			
		||||
are a party to an arrangement with a third party that is in the
 | 
			
		||||
business of distributing software, under which you make payment to the
 | 
			
		||||
third party based on the extent of your activity of conveying the
 | 
			
		||||
work, and under which the third party grants, to any of the parties
 | 
			
		||||
who would receive the covered work from you, a discriminatory patent
 | 
			
		||||
license (a) in connection with copies of the covered work conveyed by
 | 
			
		||||
you (or copies made from those copies), or (b) primarily for and in
 | 
			
		||||
connection with specific products or compilations that contain the
 | 
			
		||||
covered work, unless you entered into that arrangement, or that patent
 | 
			
		||||
license was granted, prior to 28 March 2007.
 | 
			
		||||
 | 
			
		||||
Nothing in this License shall be construed as excluding or limiting
 | 
			
		||||
any implied license or other defenses to infringement that may
 | 
			
		||||
otherwise be available to you under applicable patent law.
 | 
			
		||||
 | 
			
		||||
#### 12. No Surrender of Others' Freedom.
 | 
			
		||||
 | 
			
		||||
If conditions are imposed on you (whether by court order, agreement or
 | 
			
		||||
otherwise) that contradict the conditions of this License, they do not
 | 
			
		||||
excuse you from the conditions of this License. If you cannot convey a
 | 
			
		||||
covered work so as to satisfy simultaneously your obligations under
 | 
			
		||||
this License and any other pertinent obligations, then as a
 | 
			
		||||
consequence you may not convey it at all. For example, if you agree to
 | 
			
		||||
terms that obligate you to collect a royalty for further conveying
 | 
			
		||||
from those to whom you convey the Program, the only way you could
 | 
			
		||||
satisfy both those terms and this License would be to refrain entirely
 | 
			
		||||
from conveying the Program.
 | 
			
		||||
 | 
			
		||||
#### 13. Remote Network Interaction; Use with the GNU General Public License.
 | 
			
		||||
 | 
			
		||||
Notwithstanding any other provision of this License, if you modify the
 | 
			
		||||
Program, your modified version must prominently offer all users
 | 
			
		||||
interacting with it remotely through a computer network (if your
 | 
			
		||||
version supports such interaction) an opportunity to receive the
 | 
			
		||||
Corresponding Source of your version by providing access to the
 | 
			
		||||
Corresponding Source from a network server at no charge, through some
 | 
			
		||||
standard or customary means of facilitating copying of software. This
 | 
			
		||||
Corresponding Source shall include the Corresponding Source for any
 | 
			
		||||
work covered by version 3 of the GNU General Public License that is
 | 
			
		||||
incorporated pursuant to the following paragraph.
 | 
			
		||||
 | 
			
		||||
Notwithstanding any other provision of this License, you have
 | 
			
		||||
permission to link or combine any covered work with a work licensed
 | 
			
		||||
under version 3 of the GNU General Public License into a single
 | 
			
		||||
combined work, and to convey the resulting work. The terms of this
 | 
			
		||||
License will continue to apply to the part which is the covered work,
 | 
			
		||||
but the work with which it is combined will remain governed by version
 | 
			
		||||
3 of the GNU General Public License.
 | 
			
		||||
 | 
			
		||||
#### 14. Revised Versions of this License.
 | 
			
		||||
 | 
			
		||||
The Free Software Foundation may publish revised and/or new versions
 | 
			
		||||
of the GNU Affero General Public License from time to time. Such new
 | 
			
		||||
versions will be similar in spirit to the present version, but may
 | 
			
		||||
differ in detail to address new problems or concerns.
 | 
			
		||||
 | 
			
		||||
Each version is given a distinguishing version number. If the Program
 | 
			
		||||
specifies that a certain numbered version of the GNU Affero General
 | 
			
		||||
Public License "or any later version" applies to it, you have the
 | 
			
		||||
option of following the terms and conditions either of that numbered
 | 
			
		||||
version or of any later version published by the Free Software
 | 
			
		||||
Foundation. If the Program does not specify a version number of the
 | 
			
		||||
GNU Affero General Public License, you may choose any version ever
 | 
			
		||||
published by the Free Software Foundation.
 | 
			
		||||
 | 
			
		||||
If the Program specifies that a proxy can decide which future versions
 | 
			
		||||
of the GNU Affero General Public License can be used, that proxy's
 | 
			
		||||
public statement of acceptance of a version permanently authorizes you
 | 
			
		||||
to choose that version for the Program.
 | 
			
		||||
 | 
			
		||||
Later license versions may give you additional or different
 | 
			
		||||
permissions. However, no additional obligations are imposed on any
 | 
			
		||||
author or copyright holder as a result of your choosing to follow a
 | 
			
		||||
later version.
 | 
			
		||||
 | 
			
		||||
#### 15. Disclaimer of Warranty.
 | 
			
		||||
 | 
			
		||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
 | 
			
		||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
 | 
			
		||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
 | 
			
		||||
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
 | 
			
		||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | 
			
		||||
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
 | 
			
		||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
 | 
			
		||||
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
 | 
			
		||||
CORRECTION.
 | 
			
		||||
 | 
			
		||||
#### 16. Limitation of Liability.
 | 
			
		||||
 | 
			
		||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 | 
			
		||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
 | 
			
		||||
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
 | 
			
		||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
 | 
			
		||||
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
 | 
			
		||||
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
 | 
			
		||||
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
 | 
			
		||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
 | 
			
		||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 | 
			
		||||
 | 
			
		||||
#### 17. Interpretation of Sections 15 and 16.
 | 
			
		||||
 | 
			
		||||
If the disclaimer of warranty and limitation of liability provided
 | 
			
		||||
above cannot be given local legal effect according to their terms,
 | 
			
		||||
reviewing courts shall apply local law that most closely approximates
 | 
			
		||||
an absolute waiver of all civil liability in connection with the
 | 
			
		||||
Program, unless a warranty or assumption of liability accompanies a
 | 
			
		||||
copy of the Program in return for a fee.
 | 
			
		||||
 | 
			
		||||
END OF TERMS AND CONDITIONS
 | 
			
		||||
 | 
			
		||||
### How to Apply These Terms to Your New Programs
 | 
			
		||||
 | 
			
		||||
If you develop a new program, and you want it to be of the greatest
 | 
			
		||||
possible use to the public, the best way to achieve this is to make it
 | 
			
		||||
free software which everyone can redistribute and change under these
 | 
			
		||||
terms.
 | 
			
		||||
 | 
			
		||||
To do so, attach the following notices to the program. It is safest to
 | 
			
		||||
attach them to the start of each source file to most effectively state
 | 
			
		||||
the exclusion of warranty; and each file should have at least the
 | 
			
		||||
"copyright" line and a pointer to where the full notice is found.
 | 
			
		||||
 | 
			
		||||
        <one line to give the program's name and a brief idea of what it does.>
 | 
			
		||||
        Copyright (C) <year>  <name of author>
 | 
			
		||||
 | 
			
		||||
        This program is free software: you can redistribute it and/or modify
 | 
			
		||||
        it under the terms of the GNU Affero General Public License as
 | 
			
		||||
        published by the Free Software Foundation, either version 3 of the
 | 
			
		||||
        License, or (at your option) any later version.
 | 
			
		||||
 | 
			
		||||
        This program is distributed in the hope that it will be useful,
 | 
			
		||||
        but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
        GNU Affero General Public License for more details.
 | 
			
		||||
 | 
			
		||||
        You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
        along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
Also add information on how to contact you by electronic and paper
 | 
			
		||||
mail.
 | 
			
		||||
 | 
			
		||||
If your software can interact with users remotely through a computer
 | 
			
		||||
network, you should also make sure that it provides a way for users to
 | 
			
		||||
get its source. For example, if your program is a web application, its
 | 
			
		||||
interface could display a "Source" link that leads users to an archive
 | 
			
		||||
of the code. There are many ways you could offer source, and different
 | 
			
		||||
solutions will be better for different programs; see section 13 for
 | 
			
		||||
the specific requirements.
 | 
			
		||||
 | 
			
		||||
You should also get your employer (if you work as a programmer) or
 | 
			
		||||
school, if any, to sign a "copyright disclaimer" for the program, if
 | 
			
		||||
necessary. For more information on this, and how to apply and follow
 | 
			
		||||
the GNU AGPL, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
@@ -374,8 +374,8 @@ defmodule Memex.Accounts do
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes the signed token with the given context.
 | 
			
		||||
  """
 | 
			
		||||
  @spec delete_session_token(token :: String.t()) :: :ok
 | 
			
		||||
  def delete_session_token(token) do
 | 
			
		||||
  @spec delete_user_session_token(token :: String.t()) :: :ok
 | 
			
		||||
  def delete_user_session_token(token) do
 | 
			
		||||
    UserToken.token_and_context_query(token, "session") |> Repo.delete_all()
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
@@ -389,6 +389,17 @@ defmodule Memex.Accounts do
 | 
			
		||||
      list_users_by_role(:admin) |> Enum.empty?()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an atom representing the current configured registration mode
 | 
			
		||||
  """
 | 
			
		||||
  @spec registration_mode() :: :public | :invite_only
 | 
			
		||||
  def registration_mode do
 | 
			
		||||
    case Application.get_env(:memex, Memex.Accounts)[:registration] do
 | 
			
		||||
      "public" -> :public
 | 
			
		||||
      _other -> :invite_only
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Checks if user is an admin
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,15 @@ defmodule Memex.Email do
 | 
			
		||||
  Emails that can be sent using Swoosh.
 | 
			
		||||
 | 
			
		||||
  You can find the base email templates at
 | 
			
		||||
  `lib/memex_web/templates/layout/email.html.heex` for html emails and
 | 
			
		||||
  `lib/memex_web/templates/layout/email.txt.heex` for text emails.
 | 
			
		||||
  `lib/memex_web/components/layouts/email_html.html.heex` for html emails and
 | 
			
		||||
  `lib/memex_web/components/layouts/email_text.txt.eex` for text emails.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Phoenix.Swoosh, view: MemexWeb.EmailView, layout: {MemexWeb.LayoutView, :email}
 | 
			
		||||
  import Swoosh.Email
 | 
			
		||||
  import MemexWeb.Gettext
 | 
			
		||||
  import Phoenix.Template
 | 
			
		||||
  alias Memex.Accounts.User
 | 
			
		||||
  alias MemexWeb.EmailView
 | 
			
		||||
  alias MemexWeb.{EmailHTML, Layouts}
 | 
			
		||||
 | 
			
		||||
  @typedoc """
 | 
			
		||||
  Represents an HTML and text body email that can be sent
 | 
			
		||||
@@ -27,22 +28,29 @@ defmodule Memex.Email do
 | 
			
		||||
  @spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t()
 | 
			
		||||
  def generate_email("welcome", user, %{"url" => url}) do
 | 
			
		||||
    user
 | 
			
		||||
    |> base_email(dgettext("emails", "Confirm your Memex account"))
 | 
			
		||||
    |> render_body("confirm_email.html", %{user: user, url: url})
 | 
			
		||||
    |> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url}))
 | 
			
		||||
    |> base_email(dgettext("emails", "confirm your memEx account"))
 | 
			
		||||
    |> render_body(:confirm_email, %{user: user, url: url})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def generate_email("reset_password", user, %{"url" => url}) do
 | 
			
		||||
    user
 | 
			
		||||
    |> base_email(dgettext("emails", "Reset your Memex password"))
 | 
			
		||||
    |> render_body("reset_password.html", %{user: user, url: url})
 | 
			
		||||
    |> text_body(EmailView.render("reset_password.txt", %{user: user, url: url}))
 | 
			
		||||
    |> base_email(dgettext("emails", "reset your memEx password"))
 | 
			
		||||
    |> render_body(:reset_password, %{user: user, url: url})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def generate_email("update_email", user, %{"url" => url}) do
 | 
			
		||||
    user
 | 
			
		||||
    |> base_email(dgettext("emails", "Update your Memex email"))
 | 
			
		||||
    |> render_body("update_email.html", %{user: user, url: url})
 | 
			
		||||
    |> text_body(EmailView.render("update_email.txt", %{user: user, url: url}))
 | 
			
		||||
    |> base_email(dgettext("emails", "update your memEx email"))
 | 
			
		||||
    |> render_body(:update_email, %{user: user, url: url})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp render_body(email, template, assigns) do
 | 
			
		||||
    html_heex = apply(EmailHTML, String.to_existing_atom("#{template}_html"), [assigns])
 | 
			
		||||
    html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: html_heex)
 | 
			
		||||
 | 
			
		||||
    text_heex = apply(EmailHTML, String.to_existing_atom("#{template}_text"), [assigns])
 | 
			
		||||
    text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: text_heex)
 | 
			
		||||
 | 
			
		||||
    email |> html_body(html) |> text_body(text)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -100,13 +100,23 @@ defmodule Memex.Accounts.Invites do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer()
 | 
			
		||||
  def get_use_count(%Invite{id: invite_id}, %User{role: :admin}) do
 | 
			
		||||
    Repo.one(
 | 
			
		||||
  @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() | nil
 | 
			
		||||
  def get_use_count(%Invite{id: invite_id} = invite, user) do
 | 
			
		||||
    [invite] |> get_use_counts(user) |> Map.get(invite_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec get_use_counts([Invite.t()], User.t()) ::
 | 
			
		||||
          %{optional(Invite.id()) => non_neg_integer()}
 | 
			
		||||
  def get_use_counts(invites, %User{role: :admin}) do
 | 
			
		||||
    invite_ids = invites |> Enum.map(fn %{id: invite_id} -> invite_id end)
 | 
			
		||||
 | 
			
		||||
    Repo.all(
 | 
			
		||||
      from u in User,
 | 
			
		||||
        where: u.invite_id == ^invite_id,
 | 
			
		||||
        select: count(u.id)
 | 
			
		||||
        where: u.invite_id in ^invite_ids,
 | 
			
		||||
        group_by: u.invite_id,
 | 
			
		||||
        select: {u.invite_id, count(u.id)}
 | 
			
		||||
    )
 | 
			
		||||
    |> Map.new()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec decrement_invite_changeset(Invite.t()) :: Invite.changeset()
 | 
			
		||||
 
 | 
			
		||||
@@ -228,4 +228,13 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ defmodule Memex.Contexts.Context do
 | 
			
		||||
  alias Ecto.{Changeset, UUID}
 | 
			
		||||
  alias Memex.{Accounts.User, Repo}
 | 
			
		||||
 | 
			
		||||
  @derive {Phoenix.Param, key: :slug}
 | 
			
		||||
  @derive {Jason.Encoder,
 | 
			
		||||
           only: [
 | 
			
		||||
             :slug,
 | 
			
		||||
@@ -27,7 +28,7 @@ defmodule Memex.Contexts.Context do
 | 
			
		||||
    field :tags_string, :string, virtual: true
 | 
			
		||||
    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
			
		||||
 | 
			
		||||
    belongs_to :user, User
 | 
			
		||||
    field :user_id, :binary_id
 | 
			
		||||
 | 
			
		||||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
@@ -38,7 +39,6 @@ defmodule Memex.Contexts.Context do
 | 
			
		||||
          tags: [String.t()] | nil,
 | 
			
		||||
          tags_string: String.t() | nil,
 | 
			
		||||
          visibility: :public | :private | :unlisted,
 | 
			
		||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
			
		||||
          user_id: User.id(),
 | 
			
		||||
          inserted_at: NaiveDateTime.t(),
 | 
			
		||||
          updated_at: NaiveDateTime.t()
 | 
			
		||||
 
 | 
			
		||||
@@ -14,17 +14,17 @@ defmodule Memex.Logger do
 | 
			
		||||
      |> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
 | 
			
		||||
      |> pretty_encode()
 | 
			
		||||
 | 
			
		||||
    Logger.error(meta.reason, data: data)
 | 
			
		||||
    Logger.error("#{meta.reason} #{data}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event([:oban, :job, :start], measure, meta, _config) do
 | 
			
		||||
    data = get_oban_job_data(meta, measure) |> pretty_encode()
 | 
			
		||||
    Logger.info("Started oban job", data: data)
 | 
			
		||||
    Logger.info("Started oban job: #{data}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event([:oban, :job, :stop], measure, meta, _config) do
 | 
			
		||||
    data = get_oban_job_data(meta, measure) |> pretty_encode()
 | 
			
		||||
    Logger.info("Finished oban job", data: data)
 | 
			
		||||
    Logger.info("Finished oban job: #{data}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do
 | 
			
		||||
@@ -33,7 +33,7 @@ defmodule Memex.Logger do
 | 
			
		||||
      |> Map.put(:event, unhandled_event)
 | 
			
		||||
      |> pretty_encode()
 | 
			
		||||
 | 
			
		||||
    Logger.warning("Unhandled oban job event", data: data)
 | 
			
		||||
    Logger.warning("Unhandled oban job event: #{data}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(unhandled_event, measure, meta, config) do
 | 
			
		||||
@@ -45,7 +45,7 @@ defmodule Memex.Logger do
 | 
			
		||||
        config: config
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    Logger.warning("Unhandled telemetry event", data: data)
 | 
			
		||||
    Logger.warning("Unhandled telemetry event: #{data}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_oban_job_data(%{job: job}, measure) do
 | 
			
		||||
 
 | 
			
		||||
@@ -228,4 +228,13 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ defmodule Memex.Notes.Note do
 | 
			
		||||
  alias Ecto.{Changeset, UUID}
 | 
			
		||||
  alias Memex.{Accounts.User, Repo}
 | 
			
		||||
 | 
			
		||||
  @derive {Phoenix.Param, key: :slug}
 | 
			
		||||
  @derive {Jason.Encoder,
 | 
			
		||||
           only: [
 | 
			
		||||
             :slug,
 | 
			
		||||
@@ -26,7 +27,7 @@ defmodule Memex.Notes.Note do
 | 
			
		||||
    field :tags_string, :string, virtual: true
 | 
			
		||||
    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
			
		||||
 | 
			
		||||
    belongs_to :user, User
 | 
			
		||||
    field :user_id, :binary_id
 | 
			
		||||
 | 
			
		||||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
@@ -37,7 +38,6 @@ defmodule Memex.Notes.Note do
 | 
			
		||||
          tags: [String.t()] | nil,
 | 
			
		||||
          tags_string: String.t() | nil,
 | 
			
		||||
          visibility: :public | :private | :unlisted,
 | 
			
		||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
			
		||||
          user_id: User.id(),
 | 
			
		||||
          inserted_at: NaiveDateTime.t(),
 | 
			
		||||
          updated_at: NaiveDateTime.t()
 | 
			
		||||
 
 | 
			
		||||
@@ -230,4 +230,13 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ defmodule Memex.Pipelines.Pipeline do
 | 
			
		||||
  alias Ecto.{Changeset, UUID}
 | 
			
		||||
  alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo}
 | 
			
		||||
 | 
			
		||||
  @derive {Phoenix.Param, key: :slug}
 | 
			
		||||
  @derive {Jason.Encoder,
 | 
			
		||||
           only: [
 | 
			
		||||
             :slug,
 | 
			
		||||
@@ -27,7 +28,7 @@ defmodule Memex.Pipelines.Pipeline do
 | 
			
		||||
    field :tags_string, :string, virtual: true
 | 
			
		||||
    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
			
		||||
 | 
			
		||||
    belongs_to :user, User
 | 
			
		||||
    field :user_id, :binary_id
 | 
			
		||||
 | 
			
		||||
    has_many :steps, Step, preload_order: [asc: :position]
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +41,6 @@ defmodule Memex.Pipelines.Pipeline do
 | 
			
		||||
          tags: [String.t()] | nil,
 | 
			
		||||
          tags_string: String.t() | nil,
 | 
			
		||||
          visibility: :public | :private | :unlisted,
 | 
			
		||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
			
		||||
          user_id: User.id(),
 | 
			
		||||
          inserted_at: NaiveDateTime.t(),
 | 
			
		||||
          updated_at: NaiveDateTime.t()
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ defmodule Memex.Pipelines.Steps.Step do
 | 
			
		||||
    field :position, :integer
 | 
			
		||||
 | 
			
		||||
    belongs_to :pipeline, Pipeline
 | 
			
		||||
    belongs_to :user, User
 | 
			
		||||
    field :user_id, :binary_id
 | 
			
		||||
 | 
			
		||||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
@@ -34,7 +34,6 @@ defmodule Memex.Pipelines.Steps.Step do
 | 
			
		||||
          position: non_neg_integer(),
 | 
			
		||||
          pipeline: Pipeline.t() | Ecto.Association.NotLoaded.t(),
 | 
			
		||||
          pipeline_id: Pipeline.id(),
 | 
			
		||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
			
		||||
          user_id: User.id(),
 | 
			
		||||
          inserted_at: NaiveDateTime.t(),
 | 
			
		||||
          updated_at: NaiveDateTime.t()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										110
									
								
								lib/memex_web.ex
									
									
									
									
									
								
							
							
						
						
									
										110
									
								
								lib/memex_web.ex
									
									
									
									
									
								
							@@ -1,55 +1,61 @@
 | 
			
		||||
defmodule MemexWeb do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  The entrypoint for defining your web interface, such
 | 
			
		||||
  as controllers, views, channels and so on.
 | 
			
		||||
  as controllers, components, channels, and so on.
 | 
			
		||||
 | 
			
		||||
  This can be used in your application as:
 | 
			
		||||
 | 
			
		||||
      use MemexWeb, :controller
 | 
			
		||||
      use MemexWeb, :view
 | 
			
		||||
      use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  The definitions below will be executed for every view,
 | 
			
		||||
  controller, etc, so keep them short and clean, focused
 | 
			
		||||
  The definitions below will be executed for every controller,
 | 
			
		||||
  component, etc, so keep them short and clean, focused
 | 
			
		||||
  on imports, uses and aliases.
 | 
			
		||||
 | 
			
		||||
  Do NOT define functions inside the quoted expressions
 | 
			
		||||
  below. Instead, define any helper function in modules
 | 
			
		||||
  and import those modules here.
 | 
			
		||||
  below. Instead, define additional modules and import
 | 
			
		||||
  those modules here.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  def controller do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.Controller, namespace: MemexWeb
 | 
			
		||||
  def static_paths, do: ~w(css js fonts images favicon.ico robots.txt)
 | 
			
		||||
 | 
			
		||||
  def router do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.Router, helpers: false
 | 
			
		||||
 | 
			
		||||
      # Import common connection and controller functions to use in pipelines
 | 
			
		||||
      import Plug.Conn
 | 
			
		||||
      import MemexWeb.Gettext
 | 
			
		||||
      alias MemexWeb.Endpoint
 | 
			
		||||
      alias MemexWeb.Router.Helpers, as: Routes
 | 
			
		||||
      import Phoenix.Controller
 | 
			
		||||
      import Phoenix.LiveView.Router
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def view do
 | 
			
		||||
  def channel do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.View,
 | 
			
		||||
        root: "lib/memex_web/templates",
 | 
			
		||||
        namespace: MemexWeb
 | 
			
		||||
      use Phoenix.Channel
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
      # Import convenience functions from controllers
 | 
			
		||||
      import Phoenix.Controller,
 | 
			
		||||
        only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
 | 
			
		||||
  def controller do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.Controller,
 | 
			
		||||
        formats: [:html, :json],
 | 
			
		||||
        layouts: [html: MemexWeb.Layouts]
 | 
			
		||||
 | 
			
		||||
      # Include shared imports and aliases for views
 | 
			
		||||
      unquote(view_helpers())
 | 
			
		||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
			
		||||
      import Plug.Conn
 | 
			
		||||
      import MemexWeb.Gettext
 | 
			
		||||
 | 
			
		||||
      unquote(verified_routes())
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def live_view do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.LiveView,
 | 
			
		||||
        layout: {MemexWeb.LayoutView, "live.html"}
 | 
			
		||||
        layout: {MemexWeb.Layouts, :app}
 | 
			
		||||
 | 
			
		||||
      on_mount MemexWeb.InitAssigns
 | 
			
		||||
      unquote(view_helpers())
 | 
			
		||||
      unquote(html_helpers())
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -57,50 +63,46 @@ defmodule MemexWeb do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.LiveComponent
 | 
			
		||||
 | 
			
		||||
      unquote(view_helpers())
 | 
			
		||||
      unquote(html_helpers())
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def component do
 | 
			
		||||
  def html do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.Component
 | 
			
		||||
 | 
			
		||||
      unquote(view_helpers())
 | 
			
		||||
      # Import convenience functions from controllers
 | 
			
		||||
      import Phoenix.Controller,
 | 
			
		||||
        only: [get_csrf_token: 0, view_module: 1, view_template: 1]
 | 
			
		||||
 | 
			
		||||
      # Include general helpers for rendering HTML
 | 
			
		||||
      unquote(html_helpers())
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def router do
 | 
			
		||||
  defp html_helpers do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.Router
 | 
			
		||||
 | 
			
		||||
      import Phoenix.{Controller, LiveView.Router}
 | 
			
		||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
			
		||||
      import Plug.Conn
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def channel do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.Channel
 | 
			
		||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
			
		||||
      import MemexWeb.Gettext
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp view_helpers do
 | 
			
		||||
    quote do
 | 
			
		||||
      # Use all HTML functionality (forms, tags, etc)
 | 
			
		||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
			
		||||
      use Phoenix.HTML
 | 
			
		||||
 | 
			
		||||
      # Import LiveView and .heex helpers (live_render, link, <.form>, etc)
 | 
			
		||||
      # Import basic rendering functionality (render, render_layout, etc)
 | 
			
		||||
      import Phoenix.{Component, View}
 | 
			
		||||
      import MemexWeb.{ErrorHelpers, Gettext, LiveHelpers, ViewHelpers}
 | 
			
		||||
 | 
			
		||||
      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
			
		||||
      alias MemexWeb.Endpoint
 | 
			
		||||
      alias MemexWeb.Router.Helpers, as: Routes
 | 
			
		||||
      import Phoenix.Component
 | 
			
		||||
      import MemexWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers}
 | 
			
		||||
 | 
			
		||||
      # Shortcut for generating JS commands
 | 
			
		||||
      alias Phoenix.LiveView.JS
 | 
			
		||||
 | 
			
		||||
      # Routes generation with the ~p sigil
 | 
			
		||||
      unquote(verified_routes())
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def verified_routes do
 | 
			
		||||
    quote do
 | 
			
		||||
      use Phoenix.VerifiedRoutes,
 | 
			
		||||
        endpoint: MemexWeb.Endpoint,
 | 
			
		||||
        router: MemexWeb.Router,
 | 
			
		||||
        statics: MemexWeb.static_paths()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.Components.ContextContent do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display the content for a context
 | 
			
		||||
  """
 | 
			
		||||
  use MemexWeb, :component
 | 
			
		||||
  alias Memex.Contexts.Context
 | 
			
		||||
  alias Phoenix.HTML
 | 
			
		||||
 | 
			
		||||
  attr :context, Context, required: true
 | 
			
		||||
 | 
			
		||||
  def context_content(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      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"
 | 
			
		||||
      phx-hook="MaintainAttrs"
 | 
			
		||||
      phx-update="ignore"
 | 
			
		||||
      readonly
 | 
			
		||||
      phx-no-format
 | 
			
		||||
    ><p class="inline"><%= add_links_to_content(@context.content) %></p></div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp add_links_to_content(content) do
 | 
			
		||||
    Regex.replace(
 | 
			
		||||
      ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
			
		||||
      content,
 | 
			
		||||
      fn _whole_match, slug ->
 | 
			
		||||
        link =
 | 
			
		||||
          HTML.Link.link(
 | 
			
		||||
            "[[#{slug}]]",
 | 
			
		||||
            to: Routes.note_show_path(Endpoint, :show, slug),
 | 
			
		||||
            class: "link inline",
 | 
			
		||||
            data: [qa: "context-note-#{slug}"]
 | 
			
		||||
          )
 | 
			
		||||
          |> HTML.Safe.to_iodata()
 | 
			
		||||
          |> IO.iodata_to_binary()
 | 
			
		||||
 | 
			
		||||
        "</p>#{link}<p class=\"inline\">"
 | 
			
		||||
      end
 | 
			
		||||
    )
 | 
			
		||||
    |> HTML.raw()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -40,7 +40,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
 | 
			
		||||
      if actions == [] or current_user |> is_nil() do
 | 
			
		||||
        []
 | 
			
		||||
      else
 | 
			
		||||
        [%{label: nil, key: :actions, sortable: false}]
 | 
			
		||||
        [%{label: gettext("actions"), key: :actions, sortable: false}]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    columns = [
 | 
			
		||||
@@ -88,15 +88,9 @@ defmodule MemexWeb.Components.ContextsTableComponent do
 | 
			
		||||
 | 
			
		||||
  @spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
 | 
			
		||||
          any() | {any(), Rendered.t()}
 | 
			
		||||
  defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
 | 
			
		||||
    assigns = %{slug: slug}
 | 
			
		||||
 | 
			
		||||
  defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
 | 
			
		||||
    slug_block = ~H"""
 | 
			
		||||
    <.link
 | 
			
		||||
      navigate={Routes.context_show_path(Endpoint, :show, @slug)}
 | 
			
		||||
      class="link"
 | 
			
		||||
      data-qa={"context-show-#{@slug}"}
 | 
			
		||||
    >
 | 
			
		||||
    <.link navigate={~p"/context/#{@slug}"} class="link">
 | 
			
		||||
      <%= @slug %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    """
 | 
			
		||||
@@ -104,16 +98,10 @@ defmodule MemexWeb.Components.ContextsTableComponent do
 | 
			
		||||
    {slug, slug_block}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
 | 
			
		||||
    assigns = %{tags: tags}
 | 
			
		||||
 | 
			
		||||
  defp get_value_for_key(:tags, assigns, _additional_data) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div class="flex flex-wrap justify-center space-x-1">
 | 
			
		||||
      <.link
 | 
			
		||||
        :for={tag <- @tags}
 | 
			
		||||
        patch={Routes.context_index_path(Endpoint, :search, tag)}
 | 
			
		||||
        class="link"
 | 
			
		||||
      >
 | 
			
		||||
      <.link :for={tag <- @tags} patch={~p"/contexts/#{tag}"} class="link">
 | 
			
		||||
        <%= tag %>
 | 
			
		||||
      </.link>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										183
									
								
								lib/memex_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								lib/memex_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,183 @@
 | 
			
		||||
defmodule MemexWeb.CoreComponents do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Provides core UI components.
 | 
			
		||||
  """
 | 
			
		||||
  use Phoenix.Component
 | 
			
		||||
  use MemexWeb, :verified_routes
 | 
			
		||||
 | 
			
		||||
  import MemexWeb.{Gettext, HTMLHelpers}
 | 
			
		||||
  alias Memex.{Accounts, Accounts.Invite, Accounts.User}
 | 
			
		||||
  alias Memex.Contexts.Context
 | 
			
		||||
  alias Memex.Notes.Note
 | 
			
		||||
  alias Memex.Pipelines.Steps.Step
 | 
			
		||||
  alias Phoenix.HTML
 | 
			
		||||
  alias Phoenix.LiveView.JS
 | 
			
		||||
 | 
			
		||||
  embed_templates("core_components/*")
 | 
			
		||||
 | 
			
		||||
  attr :title_content, :string, default: nil
 | 
			
		||||
  attr :current_user, User, default: nil
 | 
			
		||||
 | 
			
		||||
  def topbar(assigns)
 | 
			
		||||
 | 
			
		||||
  attr :return_to, :string, required: true
 | 
			
		||||
  slot(:inner_block)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Renders a live component inside a modal.
 | 
			
		||||
 | 
			
		||||
  The rendered modal receives a `:return_to` option to properly update
 | 
			
		||||
  the URL when the modal is closed.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      <.modal return_to={~p"/\#{<%= schema.plural %>}"}>
 | 
			
		||||
        <.live_component
 | 
			
		||||
          module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
 | 
			
		||||
          id={@<%= schema.singular %>.id || :new}
 | 
			
		||||
          title={@page_title}
 | 
			
		||||
          action={@live_action}
 | 
			
		||||
          return_to={~p"/\#{<%= schema.singular %>}"}
 | 
			
		||||
          <%= schema.singular %>: @<%= schema.singular %>
 | 
			
		||||
        />
 | 
			
		||||
      </.modal>
 | 
			
		||||
  """
 | 
			
		||||
  def modal(assigns)
 | 
			
		||||
 | 
			
		||||
  defp hide_modal(js \\ %JS{}) do
 | 
			
		||||
    js
 | 
			
		||||
    |> JS.hide(to: "#modal", transition: "fade-out")
 | 
			
		||||
    |> JS.hide(to: "#modal-bg", transition: "fade-out")
 | 
			
		||||
    |> JS.hide(to: "#modal-content", transition: "fade-out-scale")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  attr :action, :string, required: true
 | 
			
		||||
  attr :value, :boolean, required: true
 | 
			
		||||
  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
 | 
			
		||||
  attr :use_count, :integer, default: nil
 | 
			
		||||
  attr :current_user, User, required: true
 | 
			
		||||
  slot(:inner_block)
 | 
			
		||||
  slot(:code_actions)
 | 
			
		||||
 | 
			
		||||
  def invite_card(assigns)
 | 
			
		||||
 | 
			
		||||
  attr :id, :string, required: true
 | 
			
		||||
  attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Phoenix.Component for a <time> element that renders the naivedatetime 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)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp cast_datetime(_datetime), do: ""
 | 
			
		||||
 | 
			
		||||
  attr :id, :string, required: true
 | 
			
		||||
  attr :date, :any, required: true, doc: "A `Date` struct or nil"
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Phoenix.Component for a <date> element that renders the Date in the user's
 | 
			
		||||
  local timezone
 | 
			
		||||
  """
 | 
			
		||||
  def date(assigns)
 | 
			
		||||
 | 
			
		||||
  attr :content, :string, required: true
 | 
			
		||||
  attr :filename, :string, default: "qrcode", doc: "filename without .png extension"
 | 
			
		||||
  attr :image_class, :string, default: "w-64 h-max"
 | 
			
		||||
  attr :width, :integer, default: 384, doc: "width of png to generate"
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a downloadable QR Code element
 | 
			
		||||
  """
 | 
			
		||||
  def qr_code(assigns)
 | 
			
		||||
 | 
			
		||||
  attr :note, Note, required: true
 | 
			
		||||
 | 
			
		||||
  def note_content(assigns)
 | 
			
		||||
 | 
			
		||||
  attr :context, Context, required: true
 | 
			
		||||
 | 
			
		||||
  def context_content(assigns)
 | 
			
		||||
 | 
			
		||||
  attr :step, Step, required: true
 | 
			
		||||
 | 
			
		||||
  def step_content(assigns)
 | 
			
		||||
 | 
			
		||||
  defp add_links_to_content(content, data_qa_prefix) do
 | 
			
		||||
    # replace links
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
 | 
			
		||||
          "</p>#{link}<p class=\"inline\">"
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
 | 
			
		||||
          "</p>#{link}<p class=\"inline\">"
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    content |> HTML.raw()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<div
 | 
			
		||||
  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"
 | 
			
		||||
  phx-hook="MaintainAttrs"
 | 
			
		||||
  phx-update="ignore"
 | 
			
		||||
  readonly
 | 
			
		||||
  phx-no-format
 | 
			
		||||
><p class="inline"><%= add_links_to_content(@context.content, "context-note") %></p></div>
 | 
			
		||||
							
								
								
									
										3
									
								
								lib/memex_web/components/core_components/date.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/memex_web/components/core_components/date.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
<time :if={@date} id={@id} datetime={Date.to_iso8601(@date, :extended)} phx-hook="Date">
 | 
			
		||||
  <%= Date.to_iso8601(@date, :extended) %>
 | 
			
		||||
</time>
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
 | 
			
		||||
  <%= cast_datetime(@datetime) %>
 | 
			
		||||
</time>
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
<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 %>
 | 
			
		||||
  </h1>
 | 
			
		||||
 | 
			
		||||
  <%= if @invite.disabled_at |> is_nil() 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 %>
 | 
			
		||||
    </h2>
 | 
			
		||||
  <% else %>
 | 
			
		||||
    <h2 class="title text-md">
 | 
			
		||||
      <%= gettext("invite disabled") %>
 | 
			
		||||
    </h2>
 | 
			
		||||
  <% end %>
 | 
			
		||||
 | 
			
		||||
  <.qr_code
 | 
			
		||||
    content={url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}")}
 | 
			
		||||
    filename={@invite.name}
 | 
			
		||||
  />
 | 
			
		||||
 | 
			
		||||
  <h2 :if={@use_count && @use_count != 0} class="title text-md">
 | 
			
		||||
    <%= 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"
 | 
			
		||||
      phx-no-format
 | 
			
		||||
    ><%= url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
 | 
			
		||||
    <%= 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>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										43
									
								
								lib/memex_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								lib/memex_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
<.link
 | 
			
		||||
  id="modal-bg"
 | 
			
		||||
  patch={@return_to}
 | 
			
		||||
  class="fade-in fixed z-10 left-0 top-0
 | 
			
		||||
    w-full h-full overflow-hidden
 | 
			
		||||
    p-8 flex flex-col justify-center items-center cursor-auto"
 | 
			
		||||
  style="background-color: rgba(0,0,0,0.4);"
 | 
			
		||||
  phx-remove={hide_modal()}
 | 
			
		||||
  aria-label={gettext("close modal")}
 | 
			
		||||
>
 | 
			
		||||
  <span class="hidden"></span>
 | 
			
		||||
</.link>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
  id="modal"
 | 
			
		||||
  class="fixed z-10 left-0 top-0 pointer-events-none
 | 
			
		||||
    w-full h-full overflow-hidden
 | 
			
		||||
    p-4 sm:p-8 flex flex-col justify-center items-center"
 | 
			
		||||
>
 | 
			
		||||
  <div
 | 
			
		||||
    id="modal-content"
 | 
			
		||||
    class="fade-in-scale max-w-3xl max-h-3xl relative w-full
 | 
			
		||||
      pointer-events-auto overflow-hidden
 | 
			
		||||
      px-8 py-4 sm:py-8 flex flex-col justify-start items-stretch
 | 
			
		||||
      bg-primary-800 text-primary-400 border-primary-900 border-2 rounded-lg"
 | 
			
		||||
  >
 | 
			
		||||
    <.link
 | 
			
		||||
      patch={@return_to}
 | 
			
		||||
      id="close"
 | 
			
		||||
      class="absolute top-8 right-10
 | 
			
		||||
        text-gray-500 hover:text-gray-800
 | 
			
		||||
        transition-all duration-500 ease-in-out"
 | 
			
		||||
      phx-remove={hide_modal()}
 | 
			
		||||
      aria-label={gettext("close modal")}
 | 
			
		||||
    >
 | 
			
		||||
      <i class="fa-fw fa-lg fas fa-times"></i>
 | 
			
		||||
    </.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) %>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<div
 | 
			
		||||
  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"
 | 
			
		||||
  phx-hook="MaintainAttrs"
 | 
			
		||||
  phx-update="ignore"
 | 
			
		||||
  readonly
 | 
			
		||||
  phx-no-format
 | 
			
		||||
><p class="inline"><%= add_links_to_content(@note.content, "note-link") %></p></div>
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
<a href={qr_code_image(@content)} download={@filename <> ".png"}>
 | 
			
		||||
  <img class={@image_class} alt={@filename} src={qr_code_image(@content)} />
 | 
			
		||||
</a>
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<div
 | 
			
		||||
  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"
 | 
			
		||||
  phx-hook="MaintainAttrs"
 | 
			
		||||
  phx-update="ignore"
 | 
			
		||||
  readonly
 | 
			
		||||
  phx-no-format
 | 
			
		||||
><p class="inline"><%= add_links_to_content(@step.content, "step-context") %></p></div>
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										91
									
								
								lib/memex_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								lib/memex_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-primary-400">
 | 
			
		||||
  <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") %>
 | 
			
		||||
      </.link>
 | 
			
		||||
 | 
			
		||||
      <%= if @title_content do %>
 | 
			
		||||
        <span class="mx-2 my-1">
 | 
			
		||||
          |
 | 
			
		||||
        </span>
 | 
			
		||||
        <%= @title_content %>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <hr class="mb-2 sm:hidden hr-light" />
 | 
			
		||||
 | 
			
		||||
    <ul class="flex flex-row flex-wrap justify-center items-center
 | 
			
		||||
      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") %>
 | 
			
		||||
        </.link>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
      <li class="mx-2 my-1">
 | 
			
		||||
        <.link navigate={~p"/contexts"} class="text-primary-400 hover:underline truncate">
 | 
			
		||||
          <%= gettext("contexts") %>
 | 
			
		||||
        </.link>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
      <li class="mx-2 my-1">
 | 
			
		||||
        <.link navigate={~p"/pipelines"} class="text-primary-400 hover:underline truncate">
 | 
			
		||||
          <%= 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">
 | 
			
		||||
          <.link navigate={~p"/invites"} class="text-primary-400 hover:underline">
 | 
			
		||||
            <%= 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 %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            href={~p"/users/log_out"}
 | 
			
		||||
            method="delete"
 | 
			
		||||
            data-confirm={dgettext("prompts", "are you sure you want to log out?")}
 | 
			
		||||
            aria-label={gettext("log out")}
 | 
			
		||||
          >
 | 
			
		||||
            <i class="fas fa-sign-out-alt"></i>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li
 | 
			
		||||
          :if={
 | 
			
		||||
            @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2)
 | 
			
		||||
          }
 | 
			
		||||
          class="mx-2 my-1"
 | 
			
		||||
        >
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={~p"/dashboard"}
 | 
			
		||||
            class="text-primary-400 hover:underline"
 | 
			
		||||
            aria-label={gettext("live dashboard")}
 | 
			
		||||
          >
 | 
			
		||||
            <i class="fas fa-gauge"></i>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
      <% 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") %>
 | 
			
		||||
          </.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") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
</nav>
 | 
			
		||||
							
								
								
									
										37
									
								
								lib/memex_web/components/core_components/user_card.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								lib/memex_web/components/core_components/user_card.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
<div
 | 
			
		||||
  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
 | 
			
		||||
    transition-all duration-300 ease-in-out"
 | 
			
		||||
>
 | 
			
		||||
  <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
			
		||||
    <%= @user.email %>
 | 
			
		||||
  </h1>
 | 
			
		||||
 | 
			
		||||
  <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
			
		||||
    <p>
 | 
			
		||||
      <%= if @user.confirmed_at do %>
 | 
			
		||||
        <%= gettext(
 | 
			
		||||
          "user confirmed on%{confirmed_datetime}",
 | 
			
		||||
          confirmed_datetime: ""
 | 
			
		||||
        ) %>
 | 
			
		||||
        <.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <%= gettext("email unconfirmed") %>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <p>
 | 
			
		||||
      <%= 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) %>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.Components.InviteCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for an invite
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :component
 | 
			
		||||
  alias Memex.Accounts.{Invite, Invites, User}
 | 
			
		||||
  alias MemexWeb.Endpoint
 | 
			
		||||
 | 
			
		||||
  attr :invite, Invite, required: true
 | 
			
		||||
  attr :current_user, User, required: true
 | 
			
		||||
  slot(:inner_block)
 | 
			
		||||
  slot(:code_actions)
 | 
			
		||||
 | 
			
		||||
  def invite_card(%{invite: invite, current_user: current_user} = assigns) do
 | 
			
		||||
    assigns =
 | 
			
		||||
      assigns
 | 
			
		||||
      |> assign(:use_count, Invites.get_use_count(invite, current_user))
 | 
			
		||||
      |> assign_new(:code_actions, fn -> [] end)
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <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 %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <%= if @invite.disabled_at |> is_nil() 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 %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= gettext("invite disabled") %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% end %>
 | 
			
		||||
 | 
			
		||||
      <.qr_code
 | 
			
		||||
        content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
 | 
			
		||||
        filename={@invite.name}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <h2 :if={@use_count != 0} class="title text-md">
 | 
			
		||||
        <%= 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"
 | 
			
		||||
          phx-no-format
 | 
			
		||||
        ><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
 | 
			
		||||
        <%= render_slot(@code_actions) %>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div :if={@inner_block} class="flex space-x-4 justify-center items-center">
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										17
									
								
								lib/memex_web/components/layouts.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/memex_web/components/layouts.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
defmodule MemexWeb.Layouts do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  The root layouts for the entire application
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "layouts/*"
 | 
			
		||||
 | 
			
		||||
  def get_title(%{assigns: %{title: title}}) when title not in [nil, ""] do
 | 
			
		||||
    gettext("memEx | %{title}", title: title)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_title(_conn) do
 | 
			
		||||
    gettext("memEx")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -3,11 +3,11 @@
 | 
			
		||||
    <.topbar current_user={assigns[:current_user]} />
 | 
			
		||||
 | 
			
		||||
    <div class="mx-8 my-2 flex flex-col space-y-4 text-center">
 | 
			
		||||
      <p :if={get_flash(@conn, :info)} class="alert alert-info" role="alert">
 | 
			
		||||
        <%= get_flash(@conn, :info) %>
 | 
			
		||||
      <p :if={@flash["info"]} class="alert alert-info" role="alert">
 | 
			
		||||
        <%= @flash["info"] %>
 | 
			
		||||
      </p>
 | 
			
		||||
      <p :if={get_flash(@conn, :error)} class="alert alert-danger" role="alert">
 | 
			
		||||
        <%= get_flash(@conn, :error) %>
 | 
			
		||||
      <p :if={@flash["error"]} class="alert alert-danger" role="alert">
 | 
			
		||||
        <%= @flash["error"] %>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </header>
 | 
			
		||||
@@ -9,11 +9,8 @@
 | 
			
		||||
 | 
			
		||||
    <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={Routes.live_url(Endpoint, HomeLive)}>
 | 
			
		||||
      <%= dgettext(
 | 
			
		||||
        "emails",
 | 
			
		||||
        "This email was sent from memEx"
 | 
			
		||||
      ) %>
 | 
			
		||||
    <a style="color: rgb(161, 161, 170);" href={~p"/"}>
 | 
			
		||||
      <%= dgettext("emails", "this email was sent from memEx") %>
 | 
			
		||||
    </a>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										9
									
								
								lib/memex_web/components/layouts/email_text.txt.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/memex_web/components/layouts/email_text.txt.eex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<%= @email.subject %>
 | 
			
		||||
 | 
			
		||||
====================
 | 
			
		||||
 | 
			
		||||
<%= @inner_content %>
 | 
			
		||||
 | 
			
		||||
=====================
 | 
			
		||||
 | 
			
		||||
<%= dgettext("emails", "this email was sent from memEx at %{url}", url: ~p"/") %>
 | 
			
		||||
@@ -8,13 +8,8 @@
 | 
			
		||||
    <.live_title suffix={" | #{gettext("memEx")}"}>
 | 
			
		||||
      <%= assigns[:page_title] || gettext("memEx") %>
 | 
			
		||||
    </.live_title>
 | 
			
		||||
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
 | 
			
		||||
    <script
 | 
			
		||||
      defer
 | 
			
		||||
      phx-track-static
 | 
			
		||||
      type="text/javascript"
 | 
			
		||||
      src={Routes.static_path(@conn, "/js/app.js")}
 | 
			
		||||
    >
 | 
			
		||||
    <link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
 | 
			
		||||
    <script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
 | 
			
		||||
    </script>
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.Components.NoteContent do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display the content for a note
 | 
			
		||||
  """
 | 
			
		||||
  use MemexWeb, :component
 | 
			
		||||
  alias Memex.Notes.Note
 | 
			
		||||
  alias Phoenix.HTML
 | 
			
		||||
 | 
			
		||||
  attr :note, Note, required: true
 | 
			
		||||
 | 
			
		||||
  def note_content(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      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"
 | 
			
		||||
      phx-hook="MaintainAttrs"
 | 
			
		||||
      phx-update="ignore"
 | 
			
		||||
      readonly
 | 
			
		||||
      phx-no-format
 | 
			
		||||
    ><p class="inline"><%= add_links_to_content(@note.content) %></p></div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp add_links_to_content(content) do
 | 
			
		||||
    Regex.replace(
 | 
			
		||||
      ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
			
		||||
      content,
 | 
			
		||||
      fn _whole_match, slug ->
 | 
			
		||||
        link =
 | 
			
		||||
          HTML.Link.link(
 | 
			
		||||
            "[[#{slug}]]",
 | 
			
		||||
            to: Routes.note_show_path(Endpoint, :show, slug),
 | 
			
		||||
            class: "link inline",
 | 
			
		||||
            data: [qa: "note-link-#{slug}"]
 | 
			
		||||
          )
 | 
			
		||||
          |> HTML.Safe.to_iodata()
 | 
			
		||||
          |> IO.iodata_to_binary()
 | 
			
		||||
 | 
			
		||||
        "</p>#{link}<p class=\"inline\">"
 | 
			
		||||
      end
 | 
			
		||||
    )
 | 
			
		||||
    |> HTML.raw()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -40,7 +40,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
 | 
			
		||||
      if actions == [] or current_user |> is_nil() do
 | 
			
		||||
        []
 | 
			
		||||
      else
 | 
			
		||||
        [%{label: nil, key: :actions, sortable: false}]
 | 
			
		||||
        [%{label: gettext("actions"), key: :actions, sortable: false}]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    columns = [
 | 
			
		||||
@@ -92,11 +92,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
 | 
			
		||||
    assigns = %{slug: slug}
 | 
			
		||||
 | 
			
		||||
    slug_block = ~H"""
 | 
			
		||||
    <.link
 | 
			
		||||
      navigate={Routes.note_show_path(Endpoint, :show, @slug)}
 | 
			
		||||
      class="link"
 | 
			
		||||
      data-qa={"note-show-#{@slug}"}
 | 
			
		||||
    >
 | 
			
		||||
    <.link navigate={~p"/note/#{@slug}"} class="link">
 | 
			
		||||
      <%= @slug %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    """
 | 
			
		||||
@@ -109,7 +105,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div class="flex flex-wrap justify-center space-x-1">
 | 
			
		||||
      <.link :for={tag <- @tags} patch={Routes.note_index_path(Endpoint, :search, tag)} class="link">
 | 
			
		||||
      <.link :for={tag <- @tags} patch={~p"/notes/#{tag}"} class="link">
 | 
			
		||||
        <%= tag %>
 | 
			
		||||
      </.link>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
 | 
			
		||||
      if actions == [] or current_user |> is_nil() do
 | 
			
		||||
        []
 | 
			
		||||
      else
 | 
			
		||||
        [%{label: nil, key: :actions, sortable: false}]
 | 
			
		||||
        [%{label: gettext("actions"), key: :actions, sortable: false}]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    columns = [
 | 
			
		||||
@@ -93,11 +93,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
 | 
			
		||||
    assigns = %{slug: slug}
 | 
			
		||||
 | 
			
		||||
    slug_block = ~H"""
 | 
			
		||||
    <.link
 | 
			
		||||
      navigate={Routes.pipeline_show_path(Endpoint, :show, @slug)}
 | 
			
		||||
      class="link"
 | 
			
		||||
      data-qa={"pipeline-show-#{@slug}"}
 | 
			
		||||
    >
 | 
			
		||||
    <.link navigate={~p"/pipeline/#{@slug}"} class="link">
 | 
			
		||||
      <%= @slug %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    """
 | 
			
		||||
@@ -122,11 +118,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div class="flex flex-wrap justify-center space-x-1">
 | 
			
		||||
      <.link
 | 
			
		||||
        :for={tag <- @tags}
 | 
			
		||||
        patch={Routes.pipeline_index_path(Endpoint, :search, tag)}
 | 
			
		||||
        class="link"
 | 
			
		||||
      >
 | 
			
		||||
      <.link :for={tag <- @tags} patch={~p"/pipelines/#{tag}"} class="link">
 | 
			
		||||
        <%= tag %>
 | 
			
		||||
      </.link>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.Components.StepContent do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display the content for a step
 | 
			
		||||
  """
 | 
			
		||||
  use MemexWeb, :component
 | 
			
		||||
  alias Memex.Pipelines.Steps.Step
 | 
			
		||||
  alias Phoenix.HTML
 | 
			
		||||
 | 
			
		||||
  attr :step, Step, required: true
 | 
			
		||||
 | 
			
		||||
  def step_content(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      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"
 | 
			
		||||
      phx-hook="MaintainAttrs"
 | 
			
		||||
      phx-update="ignore"
 | 
			
		||||
      readonly
 | 
			
		||||
      phx-no-format
 | 
			
		||||
    ><p class="inline"><%= add_links_to_content(@step.content) %></p></div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp add_links_to_content(content) do
 | 
			
		||||
    Regex.replace(
 | 
			
		||||
      ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
			
		||||
      content,
 | 
			
		||||
      fn _whole_match, slug ->
 | 
			
		||||
        link =
 | 
			
		||||
          HTML.Link.link(
 | 
			
		||||
            "[[#{slug}]]",
 | 
			
		||||
            to: Routes.context_show_path(Endpoint, :show, slug),
 | 
			
		||||
            class: "link inline",
 | 
			
		||||
            data: [qa: "step-context-#{slug}"]
 | 
			
		||||
          )
 | 
			
		||||
          |> HTML.Safe.to_iodata()
 | 
			
		||||
          |> IO.iodata_to_binary()
 | 
			
		||||
 | 
			
		||||
        "</p>#{link}<p class=\"inline\">"
 | 
			
		||||
      end
 | 
			
		||||
    )
 | 
			
		||||
    |> HTML.raw()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -33,7 +33,8 @@ defmodule MemexWeb.Components.TableComponent do
 | 
			
		||||
                optional(:class) => String.t(),
 | 
			
		||||
                optional(:row_class) => String.t(),
 | 
			
		||||
                optional(:alternate_row_class) => String.t(),
 | 
			
		||||
                optional(:sortable) => false
 | 
			
		||||
                optional(:sortable) => false,
 | 
			
		||||
                optional(:type) => module()
 | 
			
		||||
              }),
 | 
			
		||||
            required(:rows) =>
 | 
			
		||||
              list(%{
 | 
			
		||||
@@ -60,7 +61,8 @@ defmodule MemexWeb.Components.TableComponent do
 | 
			
		||||
        :asc
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode)
 | 
			
		||||
    type = columns |> Enum.find(%{}, fn %{key: key} -> key == initial_key end) |> Map.get(:type)
 | 
			
		||||
    rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode, type)
 | 
			
		||||
 | 
			
		||||
    socket =
 | 
			
		||||
      socket
 | 
			
		||||
@@ -68,6 +70,7 @@ defmodule MemexWeb.Components.TableComponent do
 | 
			
		||||
      |> assign(
 | 
			
		||||
        columns: columns,
 | 
			
		||||
        rows: rows,
 | 
			
		||||
        key: initial_key,
 | 
			
		||||
        last_sort_key: initial_key,
 | 
			
		||||
        sort_mode: initial_sort_mode
 | 
			
		||||
      )
 | 
			
		||||
@@ -81,7 +84,14 @@ defmodule MemexWeb.Components.TableComponent do
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "sort_by",
 | 
			
		||||
        %{"sort-key" => key},
 | 
			
		||||
        %{assigns: %{rows: rows, last_sort_key: last_sort_key, sort_mode: sort_mode}} = socket
 | 
			
		||||
        %{
 | 
			
		||||
          assigns: %{
 | 
			
		||||
            columns: columns,
 | 
			
		||||
            rows: rows,
 | 
			
		||||
            last_sort_key: last_sort_key,
 | 
			
		||||
            sort_mode: sort_mode
 | 
			
		||||
          }
 | 
			
		||||
        } = socket
 | 
			
		||||
      ) do
 | 
			
		||||
    key = key |> String.to_existing_atom()
 | 
			
		||||
 | 
			
		||||
@@ -92,11 +102,28 @@ defmodule MemexWeb.Components.TableComponent do
 | 
			
		||||
        {_new_sort_key, _last_sort_mode} -> :asc
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
 | 
			
		||||
    type =
 | 
			
		||||
      columns |> Enum.find(%{}, fn %{key: column_key} -> column_key == key end) |> Map.get(:type)
 | 
			
		||||
 | 
			
		||||
    rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode, type)
 | 
			
		||||
    {:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do
 | 
			
		||||
  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
 | 
			
		||||
       when type in [Date, DateTime] do
 | 
			
		||||
    rows
 | 
			
		||||
    |> Enum.sort_by(
 | 
			
		||||
      fn row ->
 | 
			
		||||
        case row |> Map.get(key) do
 | 
			
		||||
          {custom_sort_key, _value} -> custom_sort_key
 | 
			
		||||
          value -> value
 | 
			
		||||
        end
 | 
			
		||||
      end,
 | 
			
		||||
      {sort_mode, type}
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, _type) do
 | 
			
		||||
    rows
 | 
			
		||||
    |> Enum.sort_by(
 | 
			
		||||
      fn row ->
 | 
			
		||||
@@ -108,4 +135,25 @@ defmodule MemexWeb.Components.TableComponent do
 | 
			
		||||
      sort_mode
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Conditionally composes elements into the columns list, supports maps and
 | 
			
		||||
  lists. Works tail to front in order for efficiency
 | 
			
		||||
 | 
			
		||||
      iex> []
 | 
			
		||||
      ...> |> maybe_compose_columns(%{label: "Column 3"}, true)
 | 
			
		||||
      ...> |> maybe_compose_columns(%{label: "Column 2"}, false)
 | 
			
		||||
      ...> |> maybe_compose_columns(%{label: "Column 1"})
 | 
			
		||||
      [%{label: "Column 1"}, %{label: "Column 3"}]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec maybe_compose_columns(list(), element_to_add :: list() | map()) :: list()
 | 
			
		||||
  @spec maybe_compose_columns(list(), element_to_add :: list() | map(), boolean()) :: list()
 | 
			
		||||
  def maybe_compose_columns(columns, element_or_elements, add? \\ true)
 | 
			
		||||
 | 
			
		||||
  def maybe_compose_columns(columns, elements, true) when is_list(elements),
 | 
			
		||||
    do: Enum.concat(elements, columns)
 | 
			
		||||
 | 
			
		||||
  def maybe_compose_columns(columns, element, true) when is_map(element), do: [element | columns]
 | 
			
		||||
  def maybe_compose_columns(columns, _element_or_elements, false), do: columns
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,131 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.Components.Topbar do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Component that renders a topbar with user functions/links
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :component
 | 
			
		||||
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
  alias MemexWeb.HomeLive
 | 
			
		||||
 | 
			
		||||
  def topbar(assigns) do
 | 
			
		||||
    assigns =
 | 
			
		||||
      %{results: [], title_content: nil, flash: nil, current_user: nil} |> Map.merge(assigns)
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-primary-400">
 | 
			
		||||
      <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={Routes.live_path(Endpoint, HomeLive)}
 | 
			
		||||
            class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("memEx") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
 | 
			
		||||
          <%= if @title_content do %>
 | 
			
		||||
            <span class="mx-2 my-1">
 | 
			
		||||
              |
 | 
			
		||||
            </span>
 | 
			
		||||
            <%= @title_content %>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <hr class="mb-2 sm:hidden hr-light" />
 | 
			
		||||
 | 
			
		||||
        <ul class="flex flex-row flex-wrap justify-center items-center
 | 
			
		||||
          text-lg text-primary-400 text-ellipsis">
 | 
			
		||||
          <li class="mx-2 my-1">
 | 
			
		||||
            <.link
 | 
			
		||||
              navigate={Routes.note_index_path(Endpoint, :index)}
 | 
			
		||||
              class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
			
		||||
            >
 | 
			
		||||
              <%= gettext("notes") %>
 | 
			
		||||
            </.link>
 | 
			
		||||
          </li>
 | 
			
		||||
 | 
			
		||||
          <li class="mx-2 my-1">
 | 
			
		||||
            <.link
 | 
			
		||||
              navigate={Routes.context_index_path(Endpoint, :index)}
 | 
			
		||||
              class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
			
		||||
            >
 | 
			
		||||
              <%= gettext("contexts") %>
 | 
			
		||||
            </.link>
 | 
			
		||||
          </li>
 | 
			
		||||
 | 
			
		||||
          <li class="mx-2 my-1">
 | 
			
		||||
            <.link
 | 
			
		||||
              navigate={Routes.pipeline_index_path(Endpoint, :index)}
 | 
			
		||||
              class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
			
		||||
            >
 | 
			
		||||
              <%= 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">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.invite_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-400 text-primary-400 hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("invites") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.user_settings_path(Endpoint, :edit)}
 | 
			
		||||
                class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
			
		||||
              >
 | 
			
		||||
                <%= @current_user.email %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_session_path(Endpoint, :delete)}
 | 
			
		||||
                method="delete"
 | 
			
		||||
                data-confirm={dgettext("prompts", "are you sure you want to log out?")}
 | 
			
		||||
              >
 | 
			
		||||
                <i class="fas fa-sign-out-alt"></i>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li
 | 
			
		||||
              :if={
 | 
			
		||||
                @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2)
 | 
			
		||||
              }
 | 
			
		||||
              class="mx-2 my-1"
 | 
			
		||||
            >
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.live_dashboard_path(Endpoint, :home)}
 | 
			
		||||
                class="text-primary-400 text-primary-400 hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <i class="fas fa-gauge"></i>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
          <% else %>
 | 
			
		||||
            <li :if={Accounts.allow_registration?()} class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_registration_path(Endpoint, :new)}
 | 
			
		||||
                class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
			
		||||
              >
 | 
			
		||||
                <%= dgettext("actions", "register") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_session_path(Endpoint, :new)}
 | 
			
		||||
                class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
			
		||||
              >
 | 
			
		||||
                <%= dgettext("actions", "log in") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.Components.UserCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for a user
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :component
 | 
			
		||||
  alias Memex.Accounts.User
 | 
			
		||||
 | 
			
		||||
  attr :user, User, required: true
 | 
			
		||||
  slot(:inner_block, required: true)
 | 
			
		||||
 | 
			
		||||
  def user_card(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      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
 | 
			
		||||
        transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
			
		||||
        <%= @user.email %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
			
		||||
        <p>
 | 
			
		||||
          <%= if @user.confirmed_at do %>
 | 
			
		||||
            <%= gettext(
 | 
			
		||||
              "user confirmed on%{confirmed_datetime}",
 | 
			
		||||
              confirmed_datetime: ""
 | 
			
		||||
            ) %>
 | 
			
		||||
            <.datetime datetime={@user.confirmed_at} />
 | 
			
		||||
          <% else %>
 | 
			
		||||
            <%= gettext("email unconfirmed") %>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <p>
 | 
			
		||||
          <%= gettext(
 | 
			
		||||
            "user registered on%{registered_datetime}",
 | 
			
		||||
            registered_datetime: ""
 | 
			
		||||
          ) %>
 | 
			
		||||
          <.datetime 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) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -6,7 +6,8 @@ defmodule MemexWeb.EmailController do
 | 
			
		||||
  use MemexWeb, :controller
 | 
			
		||||
  alias Memex.Accounts.User
 | 
			
		||||
 | 
			
		||||
  plug :put_layout, {MemexWeb.LayoutView, :email}
 | 
			
		||||
  plug :put_root_layout, html: {MemexWeb.Layouts, :email_html}
 | 
			
		||||
  plug :put_layout, false
 | 
			
		||||
 | 
			
		||||
  @sample_assigns %{
 | 
			
		||||
    email: %{subject: "Example subject"},
 | 
			
		||||
@@ -18,6 +19,6 @@ defmodule MemexWeb.EmailController do
 | 
			
		||||
  Debug route used to preview emails
 | 
			
		||||
  """
 | 
			
		||||
  def preview(conn, %{"id" => template}) do
 | 
			
		||||
    render(conn, "#{template |> to_string()}.html", @sample_assigns)
 | 
			
		||||
    render(conn, String.to_existing_atom(template), @sample_assigns)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								lib/memex_web/controllers/email_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/memex_web/controllers/email_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
defmodule MemexWeb.EmailHTML do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Renders email templates
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "email_html/*.html", suffix: "_html"
 | 
			
		||||
  embed_templates "email_html/*.txt", suffix: "_text"
 | 
			
		||||
end
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
  <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
 | 
			
		||||
  <a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
 | 
			
		||||
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
@@ -9,4 +9,4 @@
 | 
			
		||||
 | 
			
		||||
<%= dgettext("emails",
 | 
			
		||||
  "If you didn't create an account at %{url}, please ignore this.",
 | 
			
		||||
  url: Routes.live_url(Endpoint, HomeLive)) %>
 | 
			
		||||
  url: ~p"/") %>
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
  <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
 | 
			
		||||
  <a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
 | 
			
		||||
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
@@ -7,4 +7,4 @@
 | 
			
		||||
 | 
			
		||||
<%= dgettext("emails",
 | 
			
		||||
  "If you didn't request this change from %{url}, please ignore this.",
 | 
			
		||||
  url: Routes.live_url(Endpoint, HomeLive)) %>
 | 
			
		||||
  url: ~p"/") %>
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
  <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
 | 
			
		||||
  <a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
 | 
			
		||||
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
@@ -7,4 +7,4 @@
 | 
			
		||||
 | 
			
		||||
<%= dgettext("emails",
 | 
			
		||||
  "If you didn't request this change from %{url}, please ignore this.",
 | 
			
		||||
  url: Routes.live_url(Endpoint, HomeLive)) %>
 | 
			
		||||
  url: ~p"/") %>
 | 
			
		||||
							
								
								
									
										16
									
								
								lib/memex_web/controllers/error_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/memex_web/controllers/error_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
defmodule MemexWeb.ErrorHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "error_html/*"
 | 
			
		||||
 | 
			
		||||
  def render(template, _assigns) do
 | 
			
		||||
    error_string =
 | 
			
		||||
      case template do
 | 
			
		||||
        "404.html" -> dgettext("errors", "not found")
 | 
			
		||||
        "401.html" -> dgettext("errors", "unauthorized")
 | 
			
		||||
        _other_path -> dgettext("errors", "internal server error")
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    error(%{error_string: error_string})
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -24,9 +24,9 @@
 | 
			
		||||
 | 
			
		||||
        <hr class="w-full hr" />
 | 
			
		||||
 | 
			
		||||
        <a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-400 text-lg">
 | 
			
		||||
        <.link href={~p"/"} class="link title text-primary-400 text-lg">
 | 
			
		||||
          <%= dgettext("errors", "go back home") %>
 | 
			
		||||
        </a>
 | 
			
		||||
        </.link>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </body>
 | 
			
		||||
							
								
								
									
										14
									
								
								lib/memex_web/controllers/error_json.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								lib/memex_web/controllers/error_json.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
defmodule MemexWeb.ErrorJSON do
 | 
			
		||||
  import MemexWeb.Gettext
 | 
			
		||||
 | 
			
		||||
  def render(template, _assigns) do
 | 
			
		||||
    error_string =
 | 
			
		||||
      case template do
 | 
			
		||||
        "404.json" -> dgettext("errors", "not found")
 | 
			
		||||
        "401.json" -> dgettext("errors", "unauthorized")
 | 
			
		||||
        _other_path -> dgettext("errors", "internal server error")
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    %{errors: %{detail: error_string}}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
defmodule MemexWeb.HomeController do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Controller for home page
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :controller
 | 
			
		||||
 | 
			
		||||
  def index(conn, _params) do
 | 
			
		||||
    render(conn, "index.html")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										5
									
								
								lib/memex_web/controllers/home_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/memex_web/controllers/home_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
defmodule MemexWeb.HomeHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "home_html/*"
 | 
			
		||||
end
 | 
			
		||||
@@ -3,12 +3,11 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
  Functions for user session and authentication
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :verified_routes
 | 
			
		||||
  import Plug.Conn
 | 
			
		||||
  import Phoenix.Controller
 | 
			
		||||
  import MemexWeb.Gettext
 | 
			
		||||
  alias Memex.{Accounts, Accounts.User}
 | 
			
		||||
  alias MemexWeb.HomeLive
 | 
			
		||||
  alias MemexWeb.Router.Helpers, as: Routes
 | 
			
		||||
 | 
			
		||||
  # Make the remember me cookie valid for 60 days.
 | 
			
		||||
  # If you want bump or reduce this value, also change
 | 
			
		||||
@@ -39,7 +38,7 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
      dgettext("errors", "You must confirm your account and log in to access this page.")
 | 
			
		||||
    )
 | 
			
		||||
    |> maybe_store_return_to()
 | 
			
		||||
    |> redirect(to: Routes.user_session_path(conn, :new))
 | 
			
		||||
    |> redirect(to: ~p"/users/log_in")
 | 
			
		||||
    |> halt()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -49,8 +48,7 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> renew_session()
 | 
			
		||||
    |> put_session(:user_token, token)
 | 
			
		||||
    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
 | 
			
		||||
    |> put_token_in_session(token)
 | 
			
		||||
    |> maybe_write_remember_me_cookie(token, params)
 | 
			
		||||
    |> redirect(to: user_return_to || signed_in_path(conn))
 | 
			
		||||
  end
 | 
			
		||||
@@ -96,7 +94,7 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
  """
 | 
			
		||||
  def log_out_user(conn) do
 | 
			
		||||
    user_token = get_session(conn, :user_token)
 | 
			
		||||
    user_token && Accounts.delete_session_token(user_token)
 | 
			
		||||
    user_token && Accounts.delete_user_session_token(user_token)
 | 
			
		||||
 | 
			
		||||
    if live_socket_id = get_session(conn, :live_socket_id) do
 | 
			
		||||
      MemexWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
 | 
			
		||||
@@ -105,7 +103,7 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
    conn
 | 
			
		||||
    |> renew_session()
 | 
			
		||||
    |> delete_resp_cookie(@remember_me_cookie)
 | 
			
		||||
    |> redirect(to: "/")
 | 
			
		||||
    |> redirect(to: ~p"/")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
@@ -119,19 +117,110 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp ensure_user_token(conn) do
 | 
			
		||||
    if user_token = get_session(conn, :user_token) do
 | 
			
		||||
      {user_token, conn}
 | 
			
		||||
    if token = get_session(conn, :user_token) do
 | 
			
		||||
      {token, conn}
 | 
			
		||||
    else
 | 
			
		||||
      conn = fetch_cookies(conn, signed: [@remember_me_cookie])
 | 
			
		||||
 | 
			
		||||
      if user_token = conn.cookies[@remember_me_cookie] do
 | 
			
		||||
        {user_token, put_session(conn, :user_token, user_token)}
 | 
			
		||||
      if token = conn.cookies[@remember_me_cookie] do
 | 
			
		||||
        {token, put_token_in_session(conn, token)}
 | 
			
		||||
      else
 | 
			
		||||
        {nil, conn}
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Handles mounting and authenticating the current_user in LiveViews.
 | 
			
		||||
 | 
			
		||||
  ## `on_mount` arguments
 | 
			
		||||
 | 
			
		||||
    * `:mount_current_user` - Assigns current_user
 | 
			
		||||
      to socket assigns based on user_token, or nil if
 | 
			
		||||
      there's no user_token or no matching user.
 | 
			
		||||
 | 
			
		||||
    * `:ensure_authenticated` - Authenticates the user from the session,
 | 
			
		||||
      and assigns the current_user to socket assigns based
 | 
			
		||||
      on user_token.
 | 
			
		||||
      Redirects to login page if there's no logged user.
 | 
			
		||||
 | 
			
		||||
    * `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
 | 
			
		||||
      Redirects to signed_in_path if there's a logged user.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
  Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
 | 
			
		||||
  the current_user:
 | 
			
		||||
 | 
			
		||||
      defmodule MemexWeb.PageLive do
 | 
			
		||||
        use MemexWeb, :live_view
 | 
			
		||||
 | 
			
		||||
        on_mount {MemexWeb.UserAuth, :mount_current_user}
 | 
			
		||||
        ...
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
  Or use the `live_session` of your router to invoke the on_mount callback:
 | 
			
		||||
 | 
			
		||||
      live_session :authenticated, on_mount: [{MemexWeb.UserAuth, :ensure_authenticated}] do
 | 
			
		||||
        live "/profile", ProfileLive, :index
 | 
			
		||||
      end
 | 
			
		||||
  """
 | 
			
		||||
  def on_mount(:mount_current_user, _params, session, socket) do
 | 
			
		||||
    {:cont, mount_current_user(session, socket)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def on_mount(:ensure_authenticated, _params, session, socket) do
 | 
			
		||||
    socket = mount_current_user(session, socket)
 | 
			
		||||
 | 
			
		||||
    if socket.assigns.current_user do
 | 
			
		||||
      {:cont, socket}
 | 
			
		||||
    else
 | 
			
		||||
      error_flash = dgettext("errors", "You must log in to access this page.")
 | 
			
		||||
 | 
			
		||||
      socket =
 | 
			
		||||
        socket
 | 
			
		||||
        |> Phoenix.LiveView.put_flash(:error, error_flash)
 | 
			
		||||
        |> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
 | 
			
		||||
 | 
			
		||||
      {:halt, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def on_mount(:ensure_admin, _params, session, socket) do
 | 
			
		||||
    socket = mount_current_user(session, socket)
 | 
			
		||||
 | 
			
		||||
    if socket.assigns.current_user && socket.assigns.current_user.role == :admin do
 | 
			
		||||
      {:cont, socket}
 | 
			
		||||
    else
 | 
			
		||||
      error_flash = dgettext("errors", "You must log in as an administrator to access this page.")
 | 
			
		||||
 | 
			
		||||
      socket =
 | 
			
		||||
        socket
 | 
			
		||||
        |> Phoenix.LiveView.put_flash(:error, error_flash)
 | 
			
		||||
        |> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
 | 
			
		||||
 | 
			
		||||
      {:halt, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
 | 
			
		||||
    socket = mount_current_user(session, socket)
 | 
			
		||||
 | 
			
		||||
    if socket.assigns.current_user do
 | 
			
		||||
      {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
 | 
			
		||||
    else
 | 
			
		||||
      {:cont, socket}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp mount_current_user(session, socket) do
 | 
			
		||||
    Phoenix.Component.assign_new(socket, :current_user, fn ->
 | 
			
		||||
      if user_token = session["user_token"] do
 | 
			
		||||
        Accounts.get_user_by_session_token(user_token)
 | 
			
		||||
      end
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Used for routes that require the user to not be authenticated.
 | 
			
		||||
  """
 | 
			
		||||
@@ -161,7 +250,7 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
        dgettext("errors", "You must confirm your account and log in to access this page.")
 | 
			
		||||
      )
 | 
			
		||||
      |> maybe_store_return_to()
 | 
			
		||||
      |> redirect(to: Routes.user_session_path(conn, :new))
 | 
			
		||||
      |> redirect(to: ~p"/users/log_in")
 | 
			
		||||
      |> halt()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
@@ -176,16 +265,34 @@ defmodule MemexWeb.UserAuth do
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
 | 
			
		||||
      |> maybe_store_return_to()
 | 
			
		||||
      |> redirect(to: Routes.live_path(conn, HomeLive))
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
      |> halt()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, _opts) do
 | 
			
		||||
    default = Application.fetch_env!(:gettext, :default_locale)
 | 
			
		||||
    Gettext.put_locale(locale || default)
 | 
			
		||||
    conn |> put_session(:locale, locale || default)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def put_user_locale(conn, _opts) do
 | 
			
		||||
    default = Application.fetch_env!(:gettext, :default_locale)
 | 
			
		||||
    Gettext.put_locale(default)
 | 
			
		||||
    conn |> put_session(:locale, default)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp put_token_in_session(conn, token) do
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_session(:user_token, token)
 | 
			
		||||
    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_store_return_to(%{method: "GET"} = conn) do
 | 
			
		||||
    put_session(conn, :user_return_to, current_path(conn))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_store_return_to(conn), do: conn
 | 
			
		||||
 | 
			
		||||
  defp signed_in_path(_conn), do: "/"
 | 
			
		||||
  defp signed_in_path(_conn), do: ~p"/"
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,14 @@ defmodule MemexWeb.UserConfirmationController do
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
 | 
			
		||||
  def new(conn, _params) do
 | 
			
		||||
    render(conn, "new.html", page_title: gettext("Confirm your account"))
 | 
			
		||||
    render(conn, :new, page_title: gettext("confirm your account"))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create(conn, %{"user" => %{"email" => email}}) do
 | 
			
		||||
    if user = Accounts.get_user_by_email(email) do
 | 
			
		||||
      Accounts.deliver_user_confirmation_instructions(
 | 
			
		||||
        user,
 | 
			
		||||
        &Routes.user_confirmation_url(conn, :confirm, &1)
 | 
			
		||||
        fn token -> url(MemexWeb.Endpoint, ~p"/users/confirm/#{token}") end
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -22,11 +22,10 @@ defmodule MemexWeb.UserConfirmationController do
 | 
			
		||||
      :info,
 | 
			
		||||
      dgettext(
 | 
			
		||||
        "prompts",
 | 
			
		||||
        "If your email is in our system and it has not been confirmed yet, " <>
 | 
			
		||||
          "you will receive an email with instructions shortly."
 | 
			
		||||
        "if your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    |> redirect(to: "/")
 | 
			
		||||
    |> redirect(to: ~p"/")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Do not log in the user after confirmation to avoid a
 | 
			
		||||
@@ -36,7 +35,7 @@ defmodule MemexWeb.UserConfirmationController do
 | 
			
		||||
      {:ok, %{email: email}} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
 | 
			
		||||
        |> redirect(to: "/")
 | 
			
		||||
        |> redirect(to: ~p"/")
 | 
			
		||||
 | 
			
		||||
      :error ->
 | 
			
		||||
        # If there is a current user and the account was already confirmed,
 | 
			
		||||
@@ -45,15 +44,15 @@ defmodule MemexWeb.UserConfirmationController do
 | 
			
		||||
        # a warning message.
 | 
			
		||||
        case conn.assigns do
 | 
			
		||||
          %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
 | 
			
		||||
            redirect(conn, to: "/")
 | 
			
		||||
            redirect(conn, to: ~p"/")
 | 
			
		||||
 | 
			
		||||
          %{} ->
 | 
			
		||||
            conn
 | 
			
		||||
            |> put_flash(
 | 
			
		||||
              :error,
 | 
			
		||||
              dgettext("errors", "User confirmation link is invalid or it has expired.")
 | 
			
		||||
              dgettext("errors", "user confirmation link is invalid or it has expired.")
 | 
			
		||||
            )
 | 
			
		||||
            |> redirect(to: "/")
 | 
			
		||||
            |> redirect(to: ~p"/")
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								lib/memex_web/controllers/user_confirmation_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/memex_web/controllers/user_confirmation_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
defmodule MemexWeb.UserConfirmationHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
 | 
			
		||||
  embed_templates "user_confirmation_html/*"
 | 
			
		||||
end
 | 
			
		||||
@@ -5,8 +5,9 @@
 | 
			
		||||
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={:user}
 | 
			
		||||
    action={Routes.user_confirmation_path(@conn, :create)}
 | 
			
		||||
    for={%{}}
 | 
			
		||||
    as={:user}
 | 
			
		||||
    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") %>
 | 
			
		||||
@@ -20,14 +21,10 @@
 | 
			
		||||
  <hr class="hr" />
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
			
		||||
    <.link
 | 
			
		||||
      :if={Accounts.allow_registration?()}
 | 
			
		||||
      href={Routes.user_registration_path(@conn, :new)}
 | 
			
		||||
      class="btn btn-primary"
 | 
			
		||||
    >
 | 
			
		||||
    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "register") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
			
		||||
    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "log in") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -2,15 +2,14 @@ defmodule MemexWeb.UserRegistrationController do
 | 
			
		||||
  use MemexWeb, :controller
 | 
			
		||||
  import MemexWeb.Gettext
 | 
			
		||||
  alias Memex.{Accounts, Accounts.Invites}
 | 
			
		||||
  alias MemexWeb.HomeLive
 | 
			
		||||
 | 
			
		||||
  def new(conn, %{"invite" => invite_token}) do
 | 
			
		||||
    if Invites.valid_invite_token?(invite_token) do
 | 
			
		||||
      conn |> render_new(invite_token)
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
 | 
			
		||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -19,14 +18,14 @@ defmodule MemexWeb.UserRegistrationController do
 | 
			
		||||
      conn |> render_new()
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
 | 
			
		||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "sorry, public registration is disabled"))
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # renders new user registration page
 | 
			
		||||
  defp render_new(conn, invite_token \\ nil) do
 | 
			
		||||
    render(conn, "new.html",
 | 
			
		||||
    render(conn, :new,
 | 
			
		||||
      changeset: Accounts.change_user_registration(),
 | 
			
		||||
      invite_token: invite_token,
 | 
			
		||||
      page_title: gettext("register")
 | 
			
		||||
@@ -38,8 +37,8 @@ defmodule MemexWeb.UserRegistrationController do
 | 
			
		||||
      conn |> create_user(attrs, invite_token)
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
 | 
			
		||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -48,8 +47,8 @@ defmodule MemexWeb.UserRegistrationController do
 | 
			
		||||
      conn |> create_user(attrs)
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
 | 
			
		||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "sorry, public registration is disabled"))
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -58,17 +57,17 @@ defmodule MemexWeb.UserRegistrationController do
 | 
			
		||||
      {:ok, user} ->
 | 
			
		||||
        Accounts.deliver_user_confirmation_instructions(
 | 
			
		||||
          user,
 | 
			
		||||
          &Routes.user_confirmation_url(conn, :confirm, &1)
 | 
			
		||||
          fn token -> url(MemexWeb.Endpoint, ~p"/users/confirm/#{token}") end
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "please check your email to verify your account"))
 | 
			
		||||
        |> redirect(to: Routes.user_session_path(Endpoint, :new))
 | 
			
		||||
        |> redirect(to: ~p"/users/log_in")
 | 
			
		||||
 | 
			
		||||
      {:error, :invalid_token} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
 | 
			
		||||
        |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
			
		||||
        |> redirect(to: ~p"/")
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        conn |> render("new.html", changeset: changeset, invite_token: invite_token)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								lib/memex_web/controllers/user_registration_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/memex_web/controllers/user_registration_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
defmodule MemexWeb.UserRegistrationHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "user_registration_html/*"
 | 
			
		||||
end
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={@changeset}
 | 
			
		||||
    action={Routes.user_registration_path(@conn, :create)}
 | 
			
		||||
    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">
 | 
			
		||||
@@ -40,10 +40,10 @@
 | 
			
		||||
  <hr class="hr" />
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
			
		||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
			
		||||
    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "log in") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
 | 
			
		||||
    <.link href={~p"/users/reset_password"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "forgot your password?") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -6,14 +6,14 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
			
		||||
  plug :get_user_by_reset_password_token when action in [:edit, :update]
 | 
			
		||||
 | 
			
		||||
  def new(conn, _params) do
 | 
			
		||||
    render(conn, "new.html", page_title: gettext("forgot your password?"))
 | 
			
		||||
    render(conn, :new, page_title: gettext("forgot your password?"))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create(conn, %{"user" => %{"email" => email}}) do
 | 
			
		||||
    if user = Accounts.get_user_by_email(email) do
 | 
			
		||||
      Accounts.deliver_user_reset_password_instructions(
 | 
			
		||||
        user,
 | 
			
		||||
        &Routes.user_reset_password_url(conn, :edit, &1)
 | 
			
		||||
        fn token -> url(MemexWeb.Endpoint, ~p"/users/reset_password/#{token}") end
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -23,17 +23,16 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
			
		||||
      :info,
 | 
			
		||||
      dgettext(
 | 
			
		||||
        "prompts",
 | 
			
		||||
        "If your email is in our system, you will receive instructions to " <>
 | 
			
		||||
          "reset your password shortly."
 | 
			
		||||
        "if your email is in our system, you will receive instructions to reset your password shortly."
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    |> redirect(to: "/")
 | 
			
		||||
    |> redirect(to: ~p"/")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def edit(conn, _params) do
 | 
			
		||||
    render(conn, "edit.html",
 | 
			
		||||
    render(conn, :edit,
 | 
			
		||||
      changeset: Accounts.change_user_password(conn.assigns.user),
 | 
			
		||||
      page_title: gettext("Reset your password")
 | 
			
		||||
      page_title: gettext("reset your password")
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -43,11 +42,11 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
			
		||||
    case Accounts.reset_user_password(conn.assigns.user, user_params) do
 | 
			
		||||
      {:ok, _} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "Password reset successfully."))
 | 
			
		||||
        |> redirect(to: Routes.user_session_path(conn, :new))
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "password reset successfully."))
 | 
			
		||||
        |> redirect(to: ~p"/users/log_in")
 | 
			
		||||
 | 
			
		||||
      {:error, changeset} ->
 | 
			
		||||
        render(conn, "edit.html", changeset: changeset)
 | 
			
		||||
        render(conn, :edit, changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -60,9 +59,9 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(
 | 
			
		||||
        :error,
 | 
			
		||||
        dgettext("errors", "Reset password link is invalid or it has expired.")
 | 
			
		||||
        dgettext("errors", "reset password link is invalid or it has expired.")
 | 
			
		||||
      )
 | 
			
		||||
      |> redirect(to: "/")
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
      |> halt()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								lib/memex_web/controllers/user_reset_password_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/memex_web/controllers/user_reset_password_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
defmodule MemexWeb.UserResetPasswordHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
 | 
			
		||||
  embed_templates "user_reset_password_html/*"
 | 
			
		||||
end
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={@changeset}
 | 
			
		||||
    action={Routes.user_reset_password_path(@conn, :update, @token)}
 | 
			
		||||
    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">
 | 
			
		||||
@@ -34,14 +34,10 @@
 | 
			
		||||
  <hr class="hr" />
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
			
		||||
    <.link
 | 
			
		||||
      :if={Accounts.allow_registration?()}
 | 
			
		||||
      href={Routes.user_registration_path(@conn, :new)}
 | 
			
		||||
      class="btn btn-primary"
 | 
			
		||||
    >
 | 
			
		||||
    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "register") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
			
		||||
    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "log in") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -5,8 +5,9 @@
 | 
			
		||||
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={:user}
 | 
			
		||||
    action={Routes.user_reset_password_path(@conn, :create)}
 | 
			
		||||
    for={%{}}
 | 
			
		||||
    as={:user}
 | 
			
		||||
    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") %>
 | 
			
		||||
@@ -20,14 +21,10 @@
 | 
			
		||||
  <hr class="hr" />
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
			
		||||
    <.link
 | 
			
		||||
      :if={Accounts.allow_registration?()}
 | 
			
		||||
      href={Routes.user_registration_path(@conn, :new)}
 | 
			
		||||
      class="btn btn-primary"
 | 
			
		||||
    >
 | 
			
		||||
    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "register") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
			
		||||
    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "log in") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -5,7 +5,7 @@ defmodule MemexWeb.UserSessionController do
 | 
			
		||||
  alias MemexWeb.UserAuth
 | 
			
		||||
 | 
			
		||||
  def new(conn, _params) do
 | 
			
		||||
    render(conn, "new.html", error_message: nil, page_title: gettext("log in"))
 | 
			
		||||
    render(conn, :new, error_message: nil, page_title: gettext("log in"))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create(conn, %{"user" => user_params}) do
 | 
			
		||||
@@ -14,7 +14,7 @@ defmodule MemexWeb.UserSessionController do
 | 
			
		||||
    if user = Accounts.get_user_by_email_and_password(email, password) do
 | 
			
		||||
      UserAuth.log_in_user(conn, user, user_params)
 | 
			
		||||
    else
 | 
			
		||||
      render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password"))
 | 
			
		||||
      render(conn, :new, error_message: dgettext("errors", "invalid email or password"))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								lib/memex_web/controllers/user_session_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/memex_web/controllers/user_session_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
defmodule MemexWeb.UserSessionHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
 | 
			
		||||
  embed_templates "user_session_html/*"
 | 
			
		||||
end
 | 
			
		||||
@@ -6,8 +6,8 @@
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={@conn}
 | 
			
		||||
    action={Routes.user_session_path(@conn, :create)}
 | 
			
		||||
    as="user"
 | 
			
		||||
    action={~p"/users/log_in"}
 | 
			
		||||
    as={:user}
 | 
			
		||||
    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">
 | 
			
		||||
@@ -31,14 +31,10 @@
 | 
			
		||||
  <hr class="hr" />
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
			
		||||
    <.link
 | 
			
		||||
      :if={Accounts.allow_registration?()}
 | 
			
		||||
      href={Routes.user_registration_path(@conn, :new)}
 | 
			
		||||
      class="btn btn-primary"
 | 
			
		||||
    >
 | 
			
		||||
    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "register") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
 | 
			
		||||
    <.link href={~p"/users/reset_password"} class="btn btn-primary">
 | 
			
		||||
      <%= dgettext("actions", "forgot your password?") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -2,12 +2,12 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
  use MemexWeb, :controller
 | 
			
		||||
  import MemexWeb.Gettext
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
  alias MemexWeb.{HomeLive, UserAuth}
 | 
			
		||||
  alias MemexWeb.UserAuth
 | 
			
		||||
 | 
			
		||||
  plug :assign_email_and_password_changesets
 | 
			
		||||
 | 
			
		||||
  def edit(conn, _params) do
 | 
			
		||||
    render(conn, "edit.html", page_title: gettext("settings"))
 | 
			
		||||
    render(conn, :edit, page_title: gettext("settings"))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update(%{assigns: %{current_user: user}} = conn, %{
 | 
			
		||||
@@ -20,7 +20,7 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
        Accounts.deliver_update_email_instructions(
 | 
			
		||||
          applied_user,
 | 
			
		||||
          user.email,
 | 
			
		||||
          &Routes.user_settings_url(conn, :confirm_email, &1)
 | 
			
		||||
          fn token -> url(MemexWeb.Endpoint, ~p"/users/settings/confirm_email/#{token}") end
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        conn
 | 
			
		||||
@@ -31,10 +31,10 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
            "a link to confirm your email change has been sent to the new address."
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
			
		||||
        |> redirect(to: ~p"/users/settings")
 | 
			
		||||
 | 
			
		||||
      {:error, changeset} ->
 | 
			
		||||
        conn |> render("edit.html", email_changeset: changeset)
 | 
			
		||||
        conn |> render(:edit, email_changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -47,11 +47,11 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
      {:ok, user} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "password updated successfully."))
 | 
			
		||||
        |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
 | 
			
		||||
        |> put_session(:user_return_to, ~p"/users/settings")
 | 
			
		||||
        |> UserAuth.log_in_user(user)
 | 
			
		||||
 | 
			
		||||
      {:error, changeset} ->
 | 
			
		||||
        conn |> render("edit.html", password_changeset: changeset)
 | 
			
		||||
        conn |> render(:edit, password_changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -63,10 +63,10 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
      {:ok, _user} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "language updated successfully."))
 | 
			
		||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
			
		||||
        |> redirect(to: ~p"/users/settings")
 | 
			
		||||
 | 
			
		||||
      {:error, changeset} ->
 | 
			
		||||
        conn |> render("edit.html", locale_changeset: changeset)
 | 
			
		||||
        conn |> render(:edit, locale_changeset: changeset)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +75,7 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
      :ok ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_flash(:info, dgettext("prompts", "email changed successfully."))
 | 
			
		||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
			
		||||
        |> redirect(to: ~p"/users/settings")
 | 
			
		||||
 | 
			
		||||
      :error ->
 | 
			
		||||
        conn
 | 
			
		||||
@@ -83,7 +83,7 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
          :error,
 | 
			
		||||
          dgettext("errors", "email change link is invalid or it has expired.")
 | 
			
		||||
        )
 | 
			
		||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
			
		||||
        |> redirect(to: ~p"/users/settings")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -93,11 +93,11 @@ defmodule MemexWeb.UserSettingsController do
 | 
			
		||||
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("prompts", "your account has been deleted"))
 | 
			
		||||
      |> redirect(to: Routes.live_path(conn, HomeLive))
 | 
			
		||||
      |> redirect(to: ~p"/")
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_flash(:error, dgettext("errors", "unable to delete user"))
 | 
			
		||||
      |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
			
		||||
      |> redirect(to: ~p"/users/settings")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								lib/memex_web/controllers/user_settings_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/memex_web/controllers/user_settings_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
defmodule MemexWeb.UserSettingsHTML do
 | 
			
		||||
  use MemexWeb, :html
 | 
			
		||||
 | 
			
		||||
  embed_templates "user_settings_html/*"
 | 
			
		||||
end
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={@email_changeset}
 | 
			
		||||
    action={Routes.user_settings_path(@conn, :update)}
 | 
			
		||||
    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"
 | 
			
		||||
  >
 | 
			
		||||
    <h3 class="title text-primary-400 text-lg text-center col-span-3">
 | 
			
		||||
@@ -50,7 +50,7 @@
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={@password_changeset}
 | 
			
		||||
    action={Routes.user_settings_path(@conn, :update)}
 | 
			
		||||
    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"
 | 
			
		||||
  >
 | 
			
		||||
    <h3 class="title text-primary-400 text-lg text-center col-span-3">
 | 
			
		||||
@@ -104,12 +104,12 @@
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={@locale_changeset}
 | 
			
		||||
    action={Routes.user_settings_path(@conn, :update)}
 | 
			
		||||
    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"
 | 
			
		||||
  >
 | 
			
		||||
    <h3 class="title text-primary-400 text-lg text-center col-span-3">
 | 
			
		||||
      <%= dgettext("actions", "change language") %>
 | 
			
		||||
    </h3>
 | 
			
		||||
    <%= 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?()}
 | 
			
		||||
@@ -134,17 +134,13 @@
 | 
			
		||||
  <hr class="hr" />
 | 
			
		||||
 | 
			
		||||
  <div class="flex justify-end items-center">
 | 
			
		||||
    <.link
 | 
			
		||||
      href={Routes.export_path(@conn, :export, :json)}
 | 
			
		||||
      class="mx-4 my-2 btn btn-primary"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
    >
 | 
			
		||||
    <.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
 | 
			
		||||
      <%= dgettext("actions", "export data as json") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
 | 
			
		||||
    <.link
 | 
			
		||||
      href={Routes.user_settings_path(@conn, :delete, @current_user)}
 | 
			
		||||
      method={:delete}
 | 
			
		||||
      href={~p"/users/settings/#{@current_user}"}
 | 
			
		||||
      method="delete"
 | 
			
		||||
      class="mx-4 my-2 btn btn-alert"
 | 
			
		||||
      data-confirm={dgettext("prompts", "are you sure you want to delete your account?")}
 | 
			
		||||
    >
 | 
			
		||||
@@ -20,7 +20,7 @@ defmodule MemexWeb.Endpoint do
 | 
			
		||||
    at: "/",
 | 
			
		||||
    from: :memex,
 | 
			
		||||
    gzip: false,
 | 
			
		||||
    only: ~w(css fonts images js favicon.ico robots.txt)
 | 
			
		||||
    only: MemexWeb.static_paths()
 | 
			
		||||
 | 
			
		||||
  # Code reloading can be explicitly enabled under the
 | 
			
		||||
  # :code_reloader configuration of your endpoint.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								lib/memex_web/html_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								lib/memex_web/html_helpers.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
defmodule MemexWeb.HTMLHelpers do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Contains common helpers that are used for rendering
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Phoenix.Component
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Displays content in a QR code as a base64 encoded PNG
 | 
			
		||||
  """
 | 
			
		||||
  @spec qr_code_image(String.t()) :: String.t()
 | 
			
		||||
  @spec qr_code_image(String.t(), width :: non_neg_integer()) :: String.t()
 | 
			
		||||
  def qr_code_image(content, width \\ 384) do
 | 
			
		||||
    img_data =
 | 
			
		||||
      content
 | 
			
		||||
      |> EQRCode.encode()
 | 
			
		||||
      |> EQRCode.png(width: width, background_color: <<24, 24, 27>>, color: <<255, 255, 255>>)
 | 
			
		||||
      |> Base.encode64()
 | 
			
		||||
 | 
			
		||||
    "data:image/png;base64," <> img_data
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,16 +1,18 @@
 | 
			
		||||
defmodule MemexWeb.ContextLive.FormComponent do
 | 
			
		||||
  use MemexWeb, :live_component
 | 
			
		||||
 | 
			
		||||
  alias Ecto.Changeset
 | 
			
		||||
  alias Memex.Contexts
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def update(%{context: context, current_user: current_user} = assigns, socket) do
 | 
			
		||||
    changeset = Contexts.change_context(context, current_user)
 | 
			
		||||
 | 
			
		||||
    {:ok,
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(assigns)
 | 
			
		||||
     |> assign(:changeset, changeset)}
 | 
			
		||||
    socket =
 | 
			
		||||
      socket
 | 
			
		||||
      |> assign(assigns)
 | 
			
		||||
      |> assign(:changeset, changeset)
 | 
			
		||||
 | 
			
		||||
    {:ok, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
@@ -19,10 +21,13 @@ defmodule MemexWeb.ContextLive.FormComponent do
 | 
			
		||||
        %{"context" => context_params},
 | 
			
		||||
        %{assigns: %{context: context, current_user: current_user}} = socket
 | 
			
		||||
      ) do
 | 
			
		||||
    changeset = context |> Contexts.change_context(context_params, current_user)
 | 
			
		||||
 | 
			
		||||
    changeset =
 | 
			
		||||
      context
 | 
			
		||||
      |> Contexts.change_context(context_params, current_user)
 | 
			
		||||
      |> Map.put(:action, :validate)
 | 
			
		||||
      case changeset |> Changeset.apply_action(:validate) do
 | 
			
		||||
        {:ok, _data} -> changeset
 | 
			
		||||
        {:error, changeset} -> changeset
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    {:noreply, assign(socket, :changeset, changeset)}
 | 
			
		||||
  end
 | 
			
		||||
@@ -37,16 +42,18 @@ defmodule MemexWeb.ContextLive.FormComponent do
 | 
			
		||||
         :edit,
 | 
			
		||||
         context_params
 | 
			
		||||
       ) do
 | 
			
		||||
    case Contexts.update_context(context, context_params, current_user) do
 | 
			
		||||
      {:ok, %{slug: slug}} ->
 | 
			
		||||
        {:noreply,
 | 
			
		||||
         socket
 | 
			
		||||
         |> put_flash(:info, gettext("%{slug} saved", slug: slug))
 | 
			
		||||
         |> push_navigate(to: return_to)}
 | 
			
		||||
    socket =
 | 
			
		||||
      case Contexts.update_context(context, context_params, current_user) do
 | 
			
		||||
        {:ok, %{slug: slug}} ->
 | 
			
		||||
          socket
 | 
			
		||||
          |> put_flash(:info, gettext("%{slug} saved", slug: slug))
 | 
			
		||||
          |> push_navigate(to: return_to)
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        {:noreply, assign(socket, :changeset, changeset)}
 | 
			
		||||
    end
 | 
			
		||||
        {:error, %Changeset{} = changeset} ->
 | 
			
		||||
          assign(socket, :changeset, changeset)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp save_context(
 | 
			
		||||
@@ -54,15 +61,17 @@ defmodule MemexWeb.ContextLive.FormComponent do
 | 
			
		||||
         :new,
 | 
			
		||||
         context_params
 | 
			
		||||
       ) do
 | 
			
		||||
    case Contexts.create_context(context_params, current_user) do
 | 
			
		||||
      {:ok, %{slug: slug}} ->
 | 
			
		||||
        {:noreply,
 | 
			
		||||
         socket
 | 
			
		||||
         |> put_flash(:info, gettext("%{slug} created", slug: slug))
 | 
			
		||||
         |> push_navigate(to: return_to)}
 | 
			
		||||
    socket =
 | 
			
		||||
      case Contexts.create_context(context_params, current_user) do
 | 
			
		||||
        {:ok, %{slug: slug}} ->
 | 
			
		||||
          socket
 | 
			
		||||
          |> put_flash(:info, gettext("%{slug} created", slug: slug))
 | 
			
		||||
          |> push_navigate(to: return_to)
 | 
			
		||||
 | 
			
		||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
			
		||||
        {:noreply, assign(socket, changeset: changeset)}
 | 
			
		||||
    end
 | 
			
		||||
        {:error, %Changeset{} = changeset} ->
 | 
			
		||||
          assign(socket, changeset: changeset)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,8 @@
 | 
			
		||||
  >
 | 
			
		||||
    <%= text_input(f, :slug,
 | 
			
		||||
      class: "input input-primary",
 | 
			
		||||
      placeholder: gettext("slug")
 | 
			
		||||
      placeholder: gettext("slug"),
 | 
			
		||||
      aria_label: gettext("slug")
 | 
			
		||||
    ) %>
 | 
			
		||||
    <%= error_tag(f, :slug) %>
 | 
			
		||||
 | 
			
		||||
@@ -20,21 +21,24 @@
 | 
			
		||||
      class: "input input-primary h-64 min-h-64",
 | 
			
		||||
      phx_hook: "MaintainAttrs",
 | 
			
		||||
      phx_update: "ignore",
 | 
			
		||||
      placeholder: gettext("use [[note-slug]] to link to a note")
 | 
			
		||||
      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) %>
 | 
			
		||||
 | 
			
		||||
    <%= text_input(f, :tags_string,
 | 
			
		||||
      id: "tags-input",
 | 
			
		||||
      class: "input input-primary",
 | 
			
		||||
      placeholder: gettext("tag1,tag2")
 | 
			
		||||
      placeholder: gettext("tag1,tag2"),
 | 
			
		||||
      aria_label: 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),
 | 
			
		||||
        class: "grow input input-primary",
 | 
			
		||||
        prompt: gettext("select privacy")
 | 
			
		||||
        prompt: gettext("select privacy"),
 | 
			
		||||
        aria_label: gettext("select privacy")
 | 
			
		||||
      ) %>
 | 
			
		||||
 | 
			
		||||
      <%= submit(dgettext("actions", "save"),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
defmodule MemexWeb.ContextLive.Index do
 | 
			
		||||
  use MemexWeb, :live_view
 | 
			
		||||
  alias Memex.{Accounts.User, Contexts, Contexts.Context}
 | 
			
		||||
  alias Memex.{Contexts, Contexts.Context}
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def mount(%{"search" => search}, _session, socket) do
 | 
			
		||||
@@ -59,14 +59,13 @@ defmodule MemexWeb.ContextLive.Index do
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
 | 
			
		||||
    {:noreply, socket |> push_patch(to: Routes.context_index_path(Endpoint, :index))}
 | 
			
		||||
    {:noreply, socket |> push_patch(to: ~p"/contexts")}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket |> push_patch(to: Routes.context_index_path(Endpoint, :search, search_term))}
 | 
			
		||||
    redirect_to = ~p"/contexts/#{search_term}"
 | 
			
		||||
    {:noreply, socket |> push_patch(to: redirect_to)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp display_contexts(%{assigns: %{current_user: current_user, search: search}} = socket)
 | 
			
		||||
@@ -77,13 +76,4 @@ defmodule MemexWeb.ContextLive.Index do
 | 
			
		||||
  defp display_contexts(%{assigns: %{search: search}} = socket) do
 | 
			
		||||
    socket |> assign(contexts: Contexts.list_public_contexts(search))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
 | 
			
		||||
  defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
			
		||||
  defp is_owner_or_admin?(_context, %{role: :admin}), do: true
 | 
			
		||||
  defp is_owner_or_admin?(_context, _other_user), do: false
 | 
			
		||||
 | 
			
		||||
  @spec is_owner?(Context.t(), User.t()) :: boolean()
 | 
			
		||||
  defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
			
		||||
  defp is_owner?(_context, _other_user), do: false
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={:search}
 | 
			
		||||
    for={%{}}
 | 
			
		||||
    as={:search}
 | 
			
		||||
    phx-change="search"
 | 
			
		||||
    phx-submit="search"
 | 
			
		||||
    class="self-stretch flex flex-col items-stretch"
 | 
			
		||||
@@ -13,6 +14,7 @@
 | 
			
		||||
    <%= text_input(f, :search_term,
 | 
			
		||||
      class: "input input-primary",
 | 
			
		||||
      value: @search,
 | 
			
		||||
      role: "search",
 | 
			
		||||
      phx_debounce: 300,
 | 
			
		||||
      placeholder: gettext("search")
 | 
			
		||||
    ) %>
 | 
			
		||||
@@ -31,19 +33,19 @@
 | 
			
		||||
    >
 | 
			
		||||
      <:actions :let={context}>
 | 
			
		||||
        <.link
 | 
			
		||||
          :if={is_owner?(context, @current_user)}
 | 
			
		||||
          patch={Routes.context_index_path(@socket, :edit, context.slug)}
 | 
			
		||||
          data-qa={"context-edit-#{context.id}"}
 | 
			
		||||
          :if={Contexts.is_owner?(context, @current_user)}
 | 
			
		||||
          patch={~p"/contexts/#{context}/edit"}
 | 
			
		||||
          aria-label={dgettext("actions", "edit %{context_slug}", context_slug: context.slug)}
 | 
			
		||||
        >
 | 
			
		||||
          <%= dgettext("actions", "edit") %>
 | 
			
		||||
        </.link>
 | 
			
		||||
        <.link
 | 
			
		||||
          :if={is_owner_or_admin?(context, @current_user)}
 | 
			
		||||
          :if={Contexts.is_owner_or_admin?(context, @current_user)}
 | 
			
		||||
          href="#"
 | 
			
		||||
          phx-click="delete"
 | 
			
		||||
          phx-value-id={context.id}
 | 
			
		||||
          data-confirm={dgettext("prompts", "are you sure?")}
 | 
			
		||||
          data-qa={"delete-context-#{context.id}"}
 | 
			
		||||
          aria-label={dgettext("actions", "delete %{context_slug}", context_slug: context.slug)}
 | 
			
		||||
        >
 | 
			
		||||
          <%= dgettext("actions", "delete") %>
 | 
			
		||||
        </.link>
 | 
			
		||||
@@ -51,16 +53,12 @@
 | 
			
		||||
    </.live_component>
 | 
			
		||||
  <% end %>
 | 
			
		||||
 | 
			
		||||
  <.link
 | 
			
		||||
    :if={@current_user}
 | 
			
		||||
    patch={Routes.context_index_path(@socket, :new)}
 | 
			
		||||
    class="self-end btn btn-primary"
 | 
			
		||||
  >
 | 
			
		||||
  <.link :if={@current_user} patch={~p"/contexts/new"} class="self-end btn btn-primary">
 | 
			
		||||
    <%= dgettext("actions", "new context") %>
 | 
			
		||||
  </.link>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<.modal :if={@live_action in [:new, :edit]} return_to={Routes.context_index_path(@socket, :index)}>
 | 
			
		||||
<.modal :if={@live_action in [:new, :edit]} return_to={~p"/contexts"}>
 | 
			
		||||
  <.live_component
 | 
			
		||||
    module={MemexWeb.ContextLive.FormComponent}
 | 
			
		||||
    id={@context.id || :new}
 | 
			
		||||
@@ -68,6 +66,6 @@
 | 
			
		||||
    title={@page_title}
 | 
			
		||||
    action={@live_action}
 | 
			
		||||
    context={@context}
 | 
			
		||||
    return_to={Routes.context_index_path(@socket, :index)}
 | 
			
		||||
    return_to={~p"/contexts"}
 | 
			
		||||
  />
 | 
			
		||||
</.modal>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
defmodule MemexWeb.ContextLive.Show do
 | 
			
		||||
  use MemexWeb, :live_view
 | 
			
		||||
  import MemexWeb.Components.ContextContent
 | 
			
		||||
  alias Memex.{Accounts.User, Contexts, Contexts.Context}
 | 
			
		||||
  alias Memex.Contexts
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def mount(_params, _session, socket) do
 | 
			
		||||
@@ -39,20 +38,11 @@ defmodule MemexWeb.ContextLive.Show do
 | 
			
		||||
    socket =
 | 
			
		||||
      socket
 | 
			
		||||
      |> put_flash(:info, gettext("%{slug} deleted", slug: slug))
 | 
			
		||||
      |> push_navigate(to: Routes.context_index_path(Endpoint, :index))
 | 
			
		||||
      |> push_navigate(to: ~p"/contexts")
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp page_title(:show, %{slug: slug}), do: slug
 | 
			
		||||
  defp page_title(:edit, %{slug: slug}), do: gettext("edit %{slug}", slug: slug)
 | 
			
		||||
 | 
			
		||||
  @spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
 | 
			
		||||
  defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
			
		||||
  defp is_owner_or_admin?(_context, %{role: :admin}), do: true
 | 
			
		||||
  defp is_owner_or_admin?(_context, _other_user), do: false
 | 
			
		||||
 | 
			
		||||
  @spec is_owner?(Context.t(), User.t()) :: boolean()
 | 
			
		||||
  defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
			
		||||
  defp is_owner?(_context, _other_user), do: false
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,7 @@
 | 
			
		||||
  </h1>
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-wrap space-x-1">
 | 
			
		||||
    <.link
 | 
			
		||||
      :for={tag <- @context.tags}
 | 
			
		||||
      navigate={Routes.context_index_path(Endpoint, :search, tag)}
 | 
			
		||||
      class="link"
 | 
			
		||||
    >
 | 
			
		||||
    <.link :for={tag <- @context.tags} navigate={~p"/contexts/#{tag}"} class="link">
 | 
			
		||||
      <%= tag %>
 | 
			
		||||
    </.link>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -20,33 +16,27 @@
 | 
			
		||||
  </p>
 | 
			
		||||
 | 
			
		||||
  <div class="self-end flex space-x-4">
 | 
			
		||||
    <.link class="btn btn-primary" navigate={Routes.context_index_path(@socket, :index)}>
 | 
			
		||||
      <%= dgettext("actions", "back") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <.link
 | 
			
		||||
      :if={is_owner?(@context, @current_user)}
 | 
			
		||||
      :if={Contexts.is_owner?(@context, @current_user)}
 | 
			
		||||
      class="btn btn-primary"
 | 
			
		||||
      patch={Routes.context_show_path(@socket, :edit, @context.slug)}
 | 
			
		||||
      patch={~p"/context/#{@context}/edit"}
 | 
			
		||||
    >
 | 
			
		||||
      <%= dgettext("actions", "edit") %>
 | 
			
		||||
    </.link>
 | 
			
		||||
    <button
 | 
			
		||||
      :if={is_owner_or_admin?(@context, @current_user)}
 | 
			
		||||
      :if={Contexts.is_owner_or_admin?(@context, @current_user)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      class="btn btn-primary"
 | 
			
		||||
      phx-click="delete"
 | 
			
		||||
      data-confirm={dgettext("prompts", "are you sure?")}
 | 
			
		||||
      data-qa={"delete-context-#{@context.id}"}
 | 
			
		||||
      aria-label={dgettext("actions", "delete %{context_slug}", context_slug: @context.slug)}
 | 
			
		||||
    >
 | 
			
		||||
      <%= dgettext("actions", "delete") %>
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<.modal
 | 
			
		||||
  :if={@live_action == :edit}
 | 
			
		||||
  return_to={Routes.context_show_path(@socket, :show, @context.slug)}
 | 
			
		||||
>
 | 
			
		||||
<.modal :if={@live_action == :edit} return_to={~p"/context/#{@context}"}>
 | 
			
		||||
  <.live_component
 | 
			
		||||
    module={MemexWeb.ContextLive.FormComponent}
 | 
			
		||||
    id={@context.id}
 | 
			
		||||
@@ -54,6 +44,6 @@
 | 
			
		||||
    title={@page_title}
 | 
			
		||||
    action={@live_action}
 | 
			
		||||
    context={@context}
 | 
			
		||||
    return_to={Routes.context_show_path(@socket, :show, @context.slug)}
 | 
			
		||||
    return_to={~p"/context/#{@context}"}
 | 
			
		||||
  />
 | 
			
		||||
</.modal>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ defmodule MemexWeb.HomeLive do
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :live_view
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
  alias MemexWeb.FaqLive
 | 
			
		||||
 | 
			
		||||
  @version Mix.Project.config()[:version]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@
 | 
			
		||||
    </li>
 | 
			
		||||
 | 
			
		||||
    <li class="flex flex-col justify-center items-center text-right space-y-2">
 | 
			
		||||
      <.link navigate={Routes.live_path(Endpoint, FaqLive)} class="btn btn-primary">
 | 
			
		||||
      <.link navigate={~p"/faq"} class="btn btn-primary">
 | 
			
		||||
        <%= gettext("read more on how to use memEx") %>
 | 
			
		||||
      </.link>
 | 
			
		||||
    </li>
 | 
			
		||||
@@ -86,13 +86,13 @@
 | 
			
		||||
      </b>
 | 
			
		||||
      <p class="flex flex-col justify-center items-center space-y-2">
 | 
			
		||||
        <%= if @admins |> Enum.empty?() do %>
 | 
			
		||||
          <.link href={Routes.user_registration_path(Endpoint, :new)} class="link">
 | 
			
		||||
          <.link href={~p"/users/register"} class="link">
 | 
			
		||||
            <%= dgettext("prompts", "register to setup memEx") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        <% else %>
 | 
			
		||||
          <a :for={%{email: email} <- @admins} class="link" href={"mailto:#{email}"}>
 | 
			
		||||
          <.link :for={%{email: email} <- @admins} class="link" href={"mailto:#{email}"}>
 | 
			
		||||
            <%= email %>
 | 
			
		||||
          </a>
 | 
			
		||||
          </.link>
 | 
			
		||||
        <% end %>
 | 
			
		||||
      </p>
 | 
			
		||||
    </li>
 | 
			
		||||
@@ -100,9 +100,9 @@
 | 
			
		||||
    <li class="flex flex-col justify-center items-center space-y-2">
 | 
			
		||||
      <b><%= gettext("registration:") %></b>
 | 
			
		||||
      <p>
 | 
			
		||||
        <%= case Application.get_env(:memex, Memex.Accounts)[:registration] do
 | 
			
		||||
          "public" -> gettext("public signups")
 | 
			
		||||
          _ -> gettext("invite only")
 | 
			
		||||
        <%= case Accounts.registration_mode() do
 | 
			
		||||
          :public -> gettext("public signups")
 | 
			
		||||
          :invite_only -> gettext("invite only")
 | 
			
		||||
        end %>
 | 
			
		||||
      </p>
 | 
			
		||||
    </li>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ defmodule MemexWeb.InviteLive.FormComponent do
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("validate", %{"invite" => invite_params}, socket) do
 | 
			
		||||
    {:noreply, socket |> assign_changeset(invite_params)}
 | 
			
		||||
    {:noreply, socket |> assign_changeset(invite_params, :validate)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
 | 
			
		||||
@@ -28,22 +28,23 @@ defmodule MemexWeb.InviteLive.FormComponent do
 | 
			
		||||
 | 
			
		||||
  defp assign_changeset(
 | 
			
		||||
         %{assigns: %{action: action, current_user: user, invite: invite}} = socket,
 | 
			
		||||
         invite_params
 | 
			
		||||
         invite_params,
 | 
			
		||||
         changeset_action \\ nil
 | 
			
		||||
       ) do
 | 
			
		||||
    changeset_action =
 | 
			
		||||
    default_action =
 | 
			
		||||
      case action do
 | 
			
		||||
        :new -> :insert
 | 
			
		||||
        :edit -> :update
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    changeset =
 | 
			
		||||
      case action do
 | 
			
		||||
        :new -> Invite.create_changeset(user, "example_token", invite_params)
 | 
			
		||||
        :edit -> invite |> Invite.update_changeset(invite_params)
 | 
			
		||||
      case default_action do
 | 
			
		||||
        :insert -> Invite.create_changeset(user, "example_token", invite_params)
 | 
			
		||||
        :update -> invite |> Invite.update_changeset(invite_params)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    changeset =
 | 
			
		||||
      case changeset |> Changeset.apply_action(changeset_action) do
 | 
			
		||||
      case changeset |> Changeset.apply_action(changeset_action || default_action) do
 | 
			
		||||
        {:ok, _data} -> changeset
 | 
			
		||||
        {:error, changeset} -> changeset
 | 
			
		||||
      end
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,12 @@
 | 
			
		||||
    <%= 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/) %>
 | 
			
		||||
      <%= 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...")
 | 
			
		||||
      phx_disable_with: dgettext("prompts", "saving...")
 | 
			
		||||
    ) %>
 | 
			
		||||
  </.form>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,24 +4,13 @@ defmodule MemexWeb.InviteLive.Index do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use MemexWeb, :live_view
 | 
			
		||||
  import MemexWeb.Components.{InviteCard, UserCard}
 | 
			
		||||
  alias Memex.Accounts
 | 
			
		||||
  alias Memex.Accounts.{Invite, Invites}
 | 
			
		||||
  alias MemexWeb.HomeLive
 | 
			
		||||
  alias Phoenix.LiveView.JS
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do
 | 
			
		||||
    socket =
 | 
			
		||||
      if current_user |> Map.get(:role) == :admin do
 | 
			
		||||
        socket |> display_invites()
 | 
			
		||||
      else
 | 
			
		||||
        prompt = dgettext("errors", "you are not authorized to view this page")
 | 
			
		||||
        return_to = Routes.live_path(Endpoint, HomeLive)
 | 
			
		||||
        socket |> put_flash(:error, prompt) |> push_redirect(to: return_to)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    {:ok, socket}
 | 
			
		||||
  def mount(_params, _session, socket) do
 | 
			
		||||
    {:ok, socket |> display_invites()}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
@@ -123,22 +112,18 @@ defmodule MemexWeb.InviteLive.Index do
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("copy_to_clipboard", _params, socket) do
 | 
			
		||||
    prompt = dgettext("prompts", "copied to clipboard")
 | 
			
		||||
    {:noreply, socket |> put_flash(:info, prompt)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "delete_user",
 | 
			
		||||
        %{"id" => id},
 | 
			
		||||
        %{assigns: %{current_user: current_user}} = socket
 | 
			
		||||
      ) do
 | 
			
		||||
    %{email: user_email} = Accounts.get_user!(id) |> Accounts.delete_user!(current_user)
 | 
			
		||||
 | 
			
		||||
    prompt = dgettext("prompts", "%{user_email} deleted succesfully", user_email: user_email)
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket |> put_flash(:info, prompt) |> display_invites()}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +136,8 @@ defmodule MemexWeb.InviteLive.Index do
 | 
			
		||||
      |> Map.get(:admin, [])
 | 
			
		||||
      |> Enum.reject(fn %{id: user_id} -> user_id == current_user.id end)
 | 
			
		||||
 | 
			
		||||
    use_counts = invites |> Invites.get_use_counts(current_user)
 | 
			
		||||
    users = all_users |> Map.get(:user, [])
 | 
			
		||||
    socket |> assign(invites: invites, admins: admins, users: users)
 | 
			
		||||
    socket |> assign(invites: invites, use_counts: use_counts, admins: admins, users: users)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user