Add contacts views
This commit is contained in:
parent
584dc08292
commit
392536fd7f
2
.gitignore
vendored
2
.gitignore
vendored
@ -35,3 +35,5 @@ send_it-*.tar
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
/assets/node_modules/
|
/assets/node_modules/
|
||||||
|
|
||||||
|
setup_env.sh
|
||||||
|
customers_master_list.csv
|
||||||
|
|||||||
@ -0,0 +1,315 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,700;1,400;1,700&family=IBM+Plex+Sans:ital,wght@0,200;0,400;0,700;1,200;1,400;1,700&family=IBM+Plex+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||||
|
@import url("./normalize.css");
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* --blue: #0064ff;*/
|
||||||
|
--grey: #d8d8d8;
|
||||||
|
--grey-dark: #1f1d1c;
|
||||||
|
--red: #ce2b2b;
|
||||||
|
--blue: #3ab0ed;
|
||||||
|
--magenta: #9b62fd;
|
||||||
|
--white: #ffffff;
|
||||||
|
--black: #000000;
|
||||||
|
|
||||||
|
--cyan: #1ee3cf;
|
||||||
|
--yellow: #eed811;
|
||||||
|
--pink: #eb0871;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: rgb(44, 43, 54);
|
||||||
|
background: radial-gradient(circle, rgba(44, 43, 54, 1) 25%, rgba(41, 41, 44, 1) 100%);
|
||||||
|
font-family: 'IBM Plex Sans', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--magenta);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.802rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.602rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.424rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.266rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.889rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-default {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
& thead {
|
||||||
|
border-bottom: 3px solid var(--magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
& th,
|
||||||
|
td {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& td.is-link:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
& tr:nth-child(2n) {
|
||||||
|
background-color: var(--grey-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
& tbody tr:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--magenta);
|
||||||
|
|
||||||
|
& a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .table-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width:600px) {
|
||||||
|
.table-default {
|
||||||
|
& tr {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr
|
||||||
|
}
|
||||||
|
|
||||||
|
& tfoot th {
|
||||||
|
text-align: unset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
label {
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=datetime-local] {
|
||||||
|
background-color: inherit;
|
||||||
|
color: inherit;
|
||||||
|
outline: none;
|
||||||
|
line-height: 1.75;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
border: 3px solid #414141;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
caret-color: var(--magenta);
|
||||||
|
background-color: inherit;
|
||||||
|
color: inherit;
|
||||||
|
outline: none;
|
||||||
|
line-height: 1.75;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
border: 3px solid #414141;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
input[type=datetime-local]:focus,
|
||||||
|
textarea:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: var(--magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--magenta);
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-default {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
accent-color: var(--magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-default {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.site-nav {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper,
|
||||||
|
.modal-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
background: var(--grey-dark);
|
||||||
|
margin: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
width: calc(72ch + 48px);
|
||||||
|
border: 0.5rem solid var(--magenta);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: var(--grey-dark);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
|
||||||
|
& h2 {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
227
assets/css/normalize.css
vendored
Normal file
227
assets/css/normalize.css
vendored
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Document
|
||||||
|
========
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Use a better box model (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji';
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1. Correct the line height in all browsers. */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2. Prevent adjustments of font size after orientation changes in iOS. */
|
||||||
|
tab-size: 4;
|
||||||
|
/* 3. Use a more readable tab size (opinionated). */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Sections
|
||||||
|
========
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
/* Remove the margin in all browsers. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Text-level semantics
|
||||||
|
====================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Add the correct font weight in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
|
||||||
|
2. Correct the odd 'em' font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family:
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
Consolas,
|
||||||
|
'Liberation Mono',
|
||||||
|
Menlo,
|
||||||
|
monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tabular data
|
||||||
|
============
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Forms
|
||||||
|
=====
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to 'inherit' in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Interactive
|
||||||
|
===========
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
@ -102,16 +102,15 @@ if config_env() == :prod do
|
|||||||
# In production you need to configure the mailer to use a different adapter.
|
# In production you need to configure the mailer to use a different adapter.
|
||||||
# Also, you may need to configure the Swoosh API client of your choice if you
|
# Also, you may need to configure the Swoosh API client of your choice if you
|
||||||
# are not using SMTP. Here is an example of the configuration:
|
# are not using SMTP. Here is an example of the configuration:
|
||||||
#
|
config :send_it, SendIt.Mailer,
|
||||||
# config :send_it, SendIt.Mailer,
|
adapter: Swoosh.Adapters.Mailgun,
|
||||||
# adapter: Swoosh.Adapters.Mailgun,
|
api_key: System.get_env("MAILGUN_API_KEY"),
|
||||||
# api_key: System.get_env("MAILGUN_API_KEY"),
|
domain: System.get_env("MAILGUN_DOMAIN")
|
||||||
# domain: System.get_env("MAILGUN_DOMAIN")
|
|
||||||
#
|
#
|
||||||
# For this example you need include a HTTP client required by Swoosh API client.
|
# For this example you need include a HTTP client required by Swoosh API client.
|
||||||
# Swoosh supports Hackney and Finch out of the box:
|
# Swoosh supports Hackney and Finch out of the box:
|
||||||
#
|
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: SendIt.Finch
|
||||||
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
|
|
||||||
#
|
#
|
||||||
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
|
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
|
||||||
end
|
end
|
||||||
|
|||||||
@ -21,6 +21,10 @@ defmodule SendIt.Marketing do
|
|||||||
Repo.all(Contact)
|
Repo.all(Contact)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_subscribed_contacts do
|
||||||
|
Repo.all(from c in Contact, where: [subscribed: true])
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single contact.
|
Gets a single contact.
|
||||||
|
|
||||||
@ -37,6 +41,8 @@ defmodule SendIt.Marketing do
|
|||||||
"""
|
"""
|
||||||
def get_contact!(id), do: Repo.get!(Contact, id)
|
def get_contact!(id), do: Repo.get!(Contact, id)
|
||||||
|
|
||||||
|
def get_contact_by_email!(email), do: Repo.get_by!(Contact, email: email)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a contact.
|
Creates a contact.
|
||||||
|
|
||||||
@ -73,6 +79,18 @@ defmodule SendIt.Marketing do
|
|||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsubscribe(email) do
|
||||||
|
case get_contact_by_email!(email) do
|
||||||
|
%Contact{} = contact ->
|
||||||
|
contact
|
||||||
|
|> Contact.changeset(%{subscribed: false})
|
||||||
|
|> Repo.update()
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a contact.
|
Deletes a contact.
|
||||||
|
|
||||||
@ -208,7 +226,7 @@ defmodule SendIt.Marketing do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
def change_message(%Message{} = message, attrs \\ %{}) do
|
def change_message(%Message{} = message, attrs \\ %{}) do
|
||||||
contacts = list_contacts()
|
contacts = list_subscribed_contacts()
|
||||||
|
|
||||||
message
|
message
|
||||||
|> Message.changeset(attrs)
|
|> Message.changeset(attrs)
|
||||||
|
|||||||
@ -47,41 +47,32 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
phx-mounted={@show && show_modal(@id)}
|
phx-mounted={@show && show_modal(@id)}
|
||||||
phx-remove={hide_modal(@id)}
|
phx-remove={hide_modal(@id)}
|
||||||
data-cancel={JS.exec(@on_cancel, "phx-remove")}
|
data-cancel={JS.exec(@on_cancel, "phx-remove")}
|
||||||
class="hidden"
|
class="modal-wrapper"
|
||||||
>
|
>
|
||||||
<div id={"#{@id}-bg"} class="transition-opacity" aria-hidden="true" />
|
|
||||||
<div
|
<div
|
||||||
class="fixed"
|
class="modal"
|
||||||
aria-labelledby={"#{@id}-title"}
|
aria-labelledby={"#{@id}-title"}
|
||||||
aria-describedby={"#{@id}-description"}
|
aria-describedby={"#{@id}-description"}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div>
|
<.focus_wrap
|
||||||
|
id={"#{@id}-container"}
|
||||||
|
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
||||||
|
phx-key="escape"
|
||||||
|
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
|
||||||
|
class="modal-container"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<.focus_wrap
|
<button phx-click={JS.exec("data-cancel", to: "##{@id}")} type="button" aria-label="close">
|
||||||
id={"#{@id}-container"}
|
×
|
||||||
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
</button>
|
||||||
phx-key="escape"
|
|
||||||
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
|
|
||||||
class="hidden transition"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
phx-click={JS.exec("data-cancel", to: "##{@id}")}
|
|
||||||
type="button"
|
|
||||||
aria-label="close"
|
|
||||||
>
|
|
||||||
<.icon name="hero-x-mark-solid" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id={"#{@id}-content"}>
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</div>
|
|
||||||
</.focus_wrap>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id={"#{@id}-content"}>
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</div>
|
||||||
|
</.focus_wrap>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -197,12 +188,10 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
|
|
||||||
def simple_form(assigns) do
|
def simple_form(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
<.form :let={f} for={@for} as={@as} {@rest} class="form-default">
|
||||||
<div>
|
<%= render_slot(@inner_block, f) %>
|
||||||
<%= render_slot(@inner_block, f) %>
|
<div :for={action <- @actions}>
|
||||||
<div :for={action <- @actions}>
|
<%= render_slot(action, f) %>
|
||||||
<%= render_slot(action, f) %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
"""
|
"""
|
||||||
@ -305,8 +294,8 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class="form-field">
|
||||||
<label>
|
<label class="checkbox">
|
||||||
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
||||||
<input type="checkbox" id={@id} name={@name} value="true" checked={@checked} {@rest} />
|
<input type="checkbox" id={@id} name={@name} value="true" checked={@checked} {@rest} />
|
||||||
<%= @label %>
|
<%= @label %>
|
||||||
@ -318,7 +307,7 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
|
|
||||||
def input(%{type: "select"} = assigns) do
|
def input(%{type: "select"} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class="form-field">
|
||||||
<.label for={@id}><%= @label %></.label>
|
<.label for={@id}><%= @label %></.label>
|
||||||
<select id={@id} name={@name} multiple={@multiple} {@rest}>
|
<select id={@id} name={@name} multiple={@multiple} {@rest}>
|
||||||
<option :if={@prompt} value=""><%= @prompt %></option>
|
<option :if={@prompt} value=""><%= @prompt %></option>
|
||||||
@ -331,7 +320,7 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
|
|
||||||
def input(%{type: "textarea"} = assigns) do
|
def input(%{type: "textarea"} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class="form-field">
|
||||||
<.label for={@id}><%= @label %></.label>
|
<.label for={@id}><%= @label %></.label>
|
||||||
<textarea
|
<textarea
|
||||||
id={@id}
|
id={@id}
|
||||||
@ -351,7 +340,7 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||||
def input(assigns) do
|
def input(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class="form-field">
|
||||||
<.label for={@id}><%= @label %></.label>
|
<.label for={@id}><%= @label %></.label>
|
||||||
<input
|
<input
|
||||||
type={@type}
|
type={@type}
|
||||||
@ -391,7 +380,7 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
|
|
||||||
def error(assigns) do
|
def error(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<p>
|
<p class="error">
|
||||||
<.icon name="hero-exclamation-circle-mini" />
|
<.icon name="hero-exclamation-circle-mini" />
|
||||||
<%= render_slot(@inner_block) %>
|
<%= render_slot(@inner_block) %>
|
||||||
</p>
|
</p>
|
||||||
@ -409,11 +398,11 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
|
|
||||||
def header(assigns) do
|
def header(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<header class={[@actions != [] && "", @class]}>
|
<header class={[@actions != [] && "", "header-default", @class]}>
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h2>
|
||||||
<%= render_slot(@inner_block) %>
|
<%= render_slot(@inner_block) %>
|
||||||
</h1>
|
</h2>
|
||||||
<p :if={@subtitle != []}>
|
<p :if={@subtitle != []}>
|
||||||
<%= render_slot(@subtitle) %>
|
<%= render_slot(@subtitle) %>
|
||||||
</p>
|
</p>
|
||||||
@ -455,40 +444,38 @@ defmodule SendItWeb.CoreComponents do
|
|||||||
end
|
end
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<table class="table-default">
|
||||||
<table>
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th :for={col <- @col}><%= col[:label] %></th>
|
||||||
<th :for={col <- @col}><%= col[:label] %></th>
|
<th :if={@action != []}>
|
||||||
<th :if={@action != []}>
|
<span>Actions</span>
|
||||||
<span>Actions</span>
|
</th>
|
||||||
</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}>
|
||||||
<tbody id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}>
|
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
<td
|
||||||
<td
|
:for={{col, i} <- Enum.with_index(@col)}
|
||||||
:for={{col, i} <- Enum.with_index(@col)}
|
phx-click={@row_click && @row_click.(row)}
|
||||||
phx-click={@row_click && @row_click.(row)}
|
class={["", @row_click && ""]}
|
||||||
class={["", @row_click && ""]}
|
>
|
||||||
>
|
<div>
|
||||||
<div>
|
<span class={["", i == 0 && ""]}>
|
||||||
<span class={["", i == 0 && ""]}>
|
<%= render_slot(col, @row_item.(row)) %>
|
||||||
<%= render_slot(col, @row_item.(row)) %>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td :if={@action != []}>
|
||||||
<td :if={@action != []}>
|
<div class="table-actions">
|
||||||
<div>
|
<span :for={action <- @action}>
|
||||||
<span :for={action <- @action}>
|
<%= render_slot(action, @row_item.(row)) %>
|
||||||
<%= render_slot(action, @row_item.(row)) %>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -12,34 +12,44 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ul>
|
<nav class="site-nav">
|
||||||
<%= if @current_user do %>
|
<h1>Send It</h1>
|
||||||
<li>
|
<ul class="nav-list">
|
||||||
<%= @current_user.email %>
|
<%= if @current_user do %>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<.link href={~p"/messages"}>
|
||||||
<.link href={~p"/users/settings"}>
|
Messages
|
||||||
Settings
|
</.link>
|
||||||
</.link>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<.link href={~p"/contacts"}>
|
||||||
<.link href={~p"/users/log_out"} method="delete">
|
Contacts
|
||||||
Log out
|
</.link>
|
||||||
</.link>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<% else %>
|
<.link href={~p"/users/settings"}>
|
||||||
<li>
|
Settings
|
||||||
<.link href={~p"/users/register"}>
|
</.link>
|
||||||
Register
|
</li>
|
||||||
</.link>
|
<li>
|
||||||
</li>
|
<.link href={~p"/users/log_out"} method="delete">
|
||||||
<li>
|
Log out
|
||||||
<.link href={~p"/users/log_in"}>
|
</.link>
|
||||||
Log in
|
</li>
|
||||||
</.link>
|
<% else %>
|
||||||
</li>
|
<li>
|
||||||
<% end %>
|
<.link href={~p"/users/register"}>
|
||||||
</ul>
|
Register
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link href={~p"/users/log_in"}>
|
||||||
|
Log in
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1 +1,4 @@
|
|||||||
<h1>Home</h1>
|
<div class="banner">
|
||||||
|
<h2>Send It</h2>
|
||||||
|
<p>Instantly compose and send a newsletter to your contacts.</p>
|
||||||
|
</div>
|
||||||
|
|||||||
84
lib/send_it_web/live/contact_live/form_component.ex
Normal file
84
lib/send_it_web/live/contact_live/form_component.ex
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
defmodule SendItWeb.ContactLive.FormComponent do
|
||||||
|
use SendItWeb, :live_component
|
||||||
|
|
||||||
|
alias SendIt.Marketing
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.header>
|
||||||
|
<%= @title %>
|
||||||
|
<:subtitle>Use this form to manage contact records in your database.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.simple_form
|
||||||
|
for={@form}
|
||||||
|
id="contact-form"
|
||||||
|
phx-target={@myself}
|
||||||
|
phx-change="validate"
|
||||||
|
phx-submit="save"
|
||||||
|
>
|
||||||
|
<.input field={@form[:name]} type="text" label="Name" />
|
||||||
|
<.input field={@form[:email]} type="text" label="Email" />
|
||||||
|
<.input field={@form[:subscribed]} type="checkbox" label="Subscribed" />
|
||||||
|
<:actions>
|
||||||
|
<.button phx-disable-with="Saving...">Save Contact</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(%{contact: contact} = assigns, socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> assign_new(:form, fn ->
|
||||||
|
to_form(Marketing.change_contact(contact))
|
||||||
|
end)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("validate", %{"contact" => contact_params}, socket) do
|
||||||
|
changeset = Marketing.change_contact(socket.assigns.contact, contact_params)
|
||||||
|
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("save", %{"contact" => contact_params}, socket) do
|
||||||
|
save_contact(socket, socket.assigns.action, contact_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_contact(socket, :edit, contact_params) do
|
||||||
|
case Marketing.update_contact(socket.assigns.contact, contact_params) do
|
||||||
|
{:ok, contact} ->
|
||||||
|
notify_parent({:saved, contact})
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Contact updated successfully")
|
||||||
|
|> push_patch(to: socket.assigns.patch)}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_contact(socket, :new, contact_params) do
|
||||||
|
case Marketing.create_contact(contact_params) do
|
||||||
|
{:ok, contact} ->
|
||||||
|
notify_parent({:saved, contact})
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Contact created successfully")
|
||||||
|
|> push_patch(to: socket.assigns.patch)}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||||
|
end
|
||||||
41
lib/send_it_web/live/contact_live/index.ex
Normal file
41
lib/send_it_web/live/contact_live/index.ex
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
defmodule SendItWeb.ContactLive.Index do
|
||||||
|
use SendItWeb, :live_view
|
||||||
|
|
||||||
|
alias SendIt.Marketing
|
||||||
|
alias SendIt.Marketing.Contact
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, stream(socket, :contacts, Marketing.list_contacts())}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(params, _url, socket) do
|
||||||
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :new, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "New Contact")
|
||||||
|
|> assign(:contact, %Contact{})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :index, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Listing Contacts")
|
||||||
|
|> assign(:contact, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({SendItWeb.ContactLive.FormComponent, {:saved, contact}}, socket) do
|
||||||
|
{:noreply, stream_insert(socket, :contacts, contact)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
|
contact = Marketing.get_contact!(id)
|
||||||
|
{:ok, _} = Marketing.delete_contact(contact)
|
||||||
|
|
||||||
|
{:noreply, stream_delete(socket, :contacts, contact)}
|
||||||
|
end
|
||||||
|
end
|
||||||
37
lib/send_it_web/live/contact_live/index.html.heex
Normal file
37
lib/send_it_web/live/contact_live/index.html.heex
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<.header>
|
||||||
|
Contacts
|
||||||
|
<:actions>
|
||||||
|
<.link patch={~p"/contacts/new"}>
|
||||||
|
<.button>New Contact</.button>
|
||||||
|
</.link>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.table
|
||||||
|
id="contacts"
|
||||||
|
rows={@streams.contacts}
|
||||||
|
row_click={fn {_id, contact} -> JS.navigate(~p"/contacts/#{contact}") end}
|
||||||
|
>
|
||||||
|
<:col :let={{_id, contact}} label="Name"><%= contact.name %></:col>
|
||||||
|
<:col :let={{_id, contact}} label="Email"><%= contact.email %></:col>
|
||||||
|
<:col :let={{_id, contact}} label="Subscribed"><%= contact.subscribed %></:col>
|
||||||
|
<:action :let={{id, contact}}>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("delete", value: %{id: contact.id}) |> hide("##{id}")}
|
||||||
|
data-confirm="Are you sure?"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</.link>
|
||||||
|
</:action>
|
||||||
|
</.table>
|
||||||
|
|
||||||
|
<.modal :if={@live_action in [:new]} id="contact-modal" show on_cancel={JS.patch(~p"/contacts")}>
|
||||||
|
<.live_component
|
||||||
|
module={SendItWeb.ContactLive.FormComponent}
|
||||||
|
id={:new}
|
||||||
|
title={@page_title}
|
||||||
|
action={@live_action}
|
||||||
|
contact={@contact}
|
||||||
|
patch={~p"/contacts"}
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
21
lib/send_it_web/live/contact_live/show.ex
Normal file
21
lib/send_it_web/live/contact_live/show.ex
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
defmodule SendItWeb.ContactLive.Show do
|
||||||
|
use SendItWeb, :live_view
|
||||||
|
|
||||||
|
alias SendIt.Marketing
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(%{"id" => id}, _, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||||
|
|> assign(:contact, Marketing.get_contact!(id))}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp page_title(:show), do: "Show Contact"
|
||||||
|
defp page_title(:edit), do: "Edit Contact"
|
||||||
|
end
|
||||||
28
lib/send_it_web/live/contact_live/show.html.heex
Normal file
28
lib/send_it_web/live/contact_live/show.html.heex
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<.header>
|
||||||
|
Contact <%= @contact.id %>
|
||||||
|
<:subtitle>This is a contact record from your database.</:subtitle>
|
||||||
|
<:actions>
|
||||||
|
<.link patch={~p"/contacts/#{@contact}/edit"} phx-click={JS.push_focus()}>
|
||||||
|
<.button>Edit contact</.button>
|
||||||
|
</.link>
|
||||||
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.list>
|
||||||
|
<:item title="Name"><%= @contact.name %></:item>
|
||||||
|
<:item title="Email"><%= @contact.email %></:item>
|
||||||
|
<:item title="Subscribed"><%= @contact.subscribed %></:item>
|
||||||
|
</.list>
|
||||||
|
|
||||||
|
<.back navigate={~p"/contacts"}>Back to contacts</.back>
|
||||||
|
|
||||||
|
<.modal :if={@live_action == :edit} id="contact-modal" show on_cancel={JS.patch(~p"/contacts/#{@contact}")}>
|
||||||
|
<.live_component
|
||||||
|
module={SendItWeb.ContactLive.FormComponent}
|
||||||
|
id={@contact.id}
|
||||||
|
title={@page_title}
|
||||||
|
action={@live_action}
|
||||||
|
contact={@contact}
|
||||||
|
patch={~p"/contacts/#{@contact}"}
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
@ -6,10 +6,10 @@ defmodule SendItWeb.MessageLive.FormComponent do
|
|||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class="flex-column">
|
||||||
<.header>
|
<.header>
|
||||||
<%= @title %>
|
<%= @title %>
|
||||||
<:subtitle>Use this form to manage message records in your database.</:subtitle>
|
<:subtitle>Messages will automatically be sent to all contacts.</:subtitle>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
@ -22,7 +22,9 @@ defmodule SendItWeb.MessageLive.FormComponent do
|
|||||||
<.input field={@form[:subject]} type="text" label="Subject" />
|
<.input field={@form[:subject]} type="text" label="Subject" />
|
||||||
<.input field={@form[:content]} type="textarea" label="Content" />
|
<.input field={@form[:content]} type="textarea" label="Content" />
|
||||||
<:actions>
|
<:actions>
|
||||||
<.button phx-disable-with="Saving...">Save Message</.button>
|
<.button data-confirm="This will send to all contacts!" phx-disable-with="Sending...">
|
||||||
|
Send Message
|
||||||
|
</.button>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.simple_form>
|
</.simple_form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<.header>
|
<.header>
|
||||||
Listing Messages
|
Messages
|
||||||
<:actions>
|
<:actions>
|
||||||
<.link patch={~p"/messages/new"}>
|
<.link patch={~p"/messages/new"}>
|
||||||
<.button>New Message</.button>
|
<.button>New Message</.button>
|
||||||
@ -15,14 +15,9 @@
|
|||||||
<:col :let={{_id, message}} label="Subject">
|
<:col :let={{_id, message}} label="Subject">
|
||||||
<%= message.subject %>
|
<%= message.subject %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={{_id, message}} label="Content">
|
<:col :let={{_id, message}} label="Created">
|
||||||
<%= message.content %>
|
<%= message.inserted_at %>
|
||||||
</:col>
|
</:col>
|
||||||
<:action :let={{_id, message}}>
|
|
||||||
<div class="sr-only">
|
|
||||||
<.link navigate={~p"/messages/#{message}"}>Show</.link>
|
|
||||||
</div>
|
|
||||||
</:action>
|
|
||||||
<:action :let={{id, message}}>
|
<:action :let={{id, message}}>
|
||||||
<.link
|
<.link
|
||||||
phx-click={JS.push("delete", value: %{id: message.id}) |> hide("##{id}")}
|
phx-click={JS.push("delete", value: %{id: message.id}) |> hide("##{id}")}
|
||||||
|
|||||||
48
lib/send_it_web/live/subscription_live.ex
Normal file
48
lib/send_it_web/live/subscription_live.ex
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
defmodule SendItWeb.SubscriptionLive do
|
||||||
|
use SendItWeb, :live_view
|
||||||
|
|
||||||
|
alias SendIt.Marketing
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, assign_form(socket)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.header>
|
||||||
|
Unsubscribe
|
||||||
|
<:subtitle>Enter your email address below to unsubscribe from all emails.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.simple_form for={@form} id="subscription-form" phx-submit="save">
|
||||||
|
<.input type="email" field={@form[:email]} />
|
||||||
|
<:actions>
|
||||||
|
<.button phx-disable-with="Saving changes...">Unsubscribe</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("save", %{"email" => email}, socket) do
|
||||||
|
unsubscribe(socket, email)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp unsubscribe(socket, email) do
|
||||||
|
case Marketing.unsubscribe(email) do
|
||||||
|
{:ok, _contact} ->
|
||||||
|
{:noreply, put_flash(socket, :info, "You have been unsubscribed from all emails.")}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:noreply, put_flash(socket, :error, reason)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_form(socket) do
|
||||||
|
assign(socket, :form, to_form(%{"email" => ""}))
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -21,6 +21,7 @@ defmodule SendItWeb.Router do
|
|||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :home
|
get "/", PageController, :home
|
||||||
|
live "/subscription", SubscriptionLive
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
@ -70,6 +71,11 @@ defmodule SendItWeb.Router do
|
|||||||
live "/messages/new", MessageLive.Index, :new
|
live "/messages/new", MessageLive.Index, :new
|
||||||
live "/messages/:id", MessageLive.Show, :show
|
live "/messages/:id", MessageLive.Show, :show
|
||||||
|
|
||||||
|
live "/contacts", ContactLive.Index, :index
|
||||||
|
live "/contacts/new", ContactLive.Index, :new
|
||||||
|
live "/contacts/:id", ContactLive.Show, :show
|
||||||
|
live "/contacts/:id/edit", ContactLive.Show, :edit
|
||||||
|
|
||||||
live "/users/settings", UserSettingsLive, :edit
|
live "/users/settings", UserSettingsLive, :edit
|
||||||
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||||
end
|
end
|
||||||
|
|||||||
3
mix.exs
3
mix.exs
@ -50,7 +50,8 @@ defmodule SendIt.MixProject do
|
|||||||
{:telemetry_poller, "~> 1.0"},
|
{:telemetry_poller, "~> 1.0"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.1.1"},
|
{:dns_cluster, "~> 0.1.1"},
|
||||||
{:bandit, "~> 1.5"}
|
{:bandit, "~> 1.5"},
|
||||||
|
{:multipart, "~> 0.4.0"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
1
mix.lock
1
mix.lock
@ -18,6 +18,7 @@
|
|||||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||||
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||||
|
"multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||||
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
||||||
|
|||||||
113
test/send_it_web/live/contact_live_test.exs
Normal file
113
test/send_it_web/live/contact_live_test.exs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
defmodule SendItWeb.ContactLiveTest do
|
||||||
|
use SendItWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import SendIt.MarketingFixtures
|
||||||
|
|
||||||
|
@create_attrs %{name: "some name", email: "some email", subscribed: true}
|
||||||
|
@update_attrs %{name: "some updated name", email: "some updated email", subscribed: false}
|
||||||
|
@invalid_attrs %{name: nil, email: nil, subscribed: false}
|
||||||
|
|
||||||
|
defp create_contact(_) do
|
||||||
|
contact = contact_fixture()
|
||||||
|
%{contact: contact}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Index" do
|
||||||
|
setup [:create_contact]
|
||||||
|
|
||||||
|
test "lists all contacts", %{conn: conn, contact: contact} do
|
||||||
|
{:ok, _index_live, html} = live(conn, ~p"/contacts")
|
||||||
|
|
||||||
|
assert html =~ "Listing Contacts"
|
||||||
|
assert html =~ contact.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves new contact", %{conn: conn} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/contacts")
|
||||||
|
|
||||||
|
assert index_live |> element("a", "New Contact") |> render_click() =~
|
||||||
|
"New Contact"
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/contacts/new")
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#contact-form", contact: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#contact-form", contact: @create_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/contacts")
|
||||||
|
|
||||||
|
html = render(index_live)
|
||||||
|
assert html =~ "Contact created successfully"
|
||||||
|
assert html =~ "some name"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates contact in listing", %{conn: conn, contact: contact} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/contacts")
|
||||||
|
|
||||||
|
assert index_live |> element("#contacts-#{contact.id} a", "Edit") |> render_click() =~
|
||||||
|
"Edit Contact"
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/contacts/#{contact}/edit")
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#contact-form", contact: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#contact-form", contact: @update_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/contacts")
|
||||||
|
|
||||||
|
html = render(index_live)
|
||||||
|
assert html =~ "Contact updated successfully"
|
||||||
|
assert html =~ "some updated name"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes contact in listing", %{conn: conn, contact: contact} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/contacts")
|
||||||
|
|
||||||
|
assert index_live |> element("#contacts-#{contact.id} a", "Delete") |> render_click()
|
||||||
|
refute has_element?(index_live, "#contacts-#{contact.id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Show" do
|
||||||
|
setup [:create_contact]
|
||||||
|
|
||||||
|
test "displays contact", %{conn: conn, contact: contact} do
|
||||||
|
{:ok, _show_live, html} = live(conn, ~p"/contacts/#{contact}")
|
||||||
|
|
||||||
|
assert html =~ "Show Contact"
|
||||||
|
assert html =~ contact.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates contact within modal", %{conn: conn, contact: contact} do
|
||||||
|
{:ok, show_live, _html} = live(conn, ~p"/contacts/#{contact}")
|
||||||
|
|
||||||
|
assert show_live |> element("a", "Edit") |> render_click() =~
|
||||||
|
"Edit Contact"
|
||||||
|
|
||||||
|
assert_patch(show_live, ~p"/contacts/#{contact}/show/edit")
|
||||||
|
|
||||||
|
assert show_live
|
||||||
|
|> form("#contact-form", contact: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert show_live
|
||||||
|
|> form("#contact-form", contact: @update_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(show_live, ~p"/contacts/#{contact}")
|
||||||
|
|
||||||
|
html = render(show_live)
|
||||||
|
assert html =~ "Contact updated successfully"
|
||||||
|
assert html =~ "some updated name"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user