Skip to content

Commit c08ca14

Browse files
committed
Add subscription forms
1 parent b205761 commit c08ca14

File tree

9 files changed

+264
-56
lines changed

9 files changed

+264
-56
lines changed

frontend/src/Forms.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from "react"
2+
import {
3+
Row,
4+
Col,
5+
Checkbox,
6+
} from "antd"
7+
8+
import * as cs from "./constants"
9+
10+
class Forms extends React.PureComponent {
11+
state = {
12+
lists: [],
13+
selected: []
14+
}
15+
16+
componentDidMount() {
17+
this.props.pageTitle("Forms")
18+
this.props
19+
.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
20+
per_page: "all"
21+
})
22+
.then(() => {
23+
this.setState({ lists: this.props.data[cs.ModelLists].results })
24+
})
25+
}
26+
27+
handleSelection(sel) {
28+
let out = []
29+
sel.forEach(s => {
30+
const item = this.state.lists.find(l => {
31+
return l.uuid === s
32+
})
33+
if (item) {
34+
out.push(item)
35+
}
36+
})
37+
38+
console.log(out)
39+
this.setState({ selected: out })
40+
}
41+
42+
render() {
43+
return (
44+
<section className="content list-form">
45+
<h1>Forms</h1>
46+
<Row>
47+
<Col span={8}>
48+
<Checkbox.Group
49+
className="lists"
50+
options={this.state.lists.map(l => {
51+
return { label: l.name, value: l.uuid }
52+
})}
53+
defaultValue={[]}
54+
onChange={(sel) => this.handleSelection(sel)}
55+
/>
56+
</Col>
57+
<Col span={16}>
58+
<h1>Form HTML</h1>
59+
<p>Use the following HTML to show a subscription form on an external webpage.</p>
60+
<p>
61+
The form should have the <code><strong>email</strong></code> field and one or more{" "}
62+
<code><strong>l</strong></code> (list UUID) fields. The <code><strong>name</strong></code> field is optional.
63+
</p>
64+
<pre className="html">
65+
66+
{`<form method="post" action="${window.CONFIG.rootURL}/subscription/form" class="listmonk-subscription">
67+
<div>
68+
<h3>Subscribe</h3>
69+
<p><input type="text" name="email" value="" placeholder="E-mail" /></p>
70+
<p><input type="text" name="name" value="" placeholder="Name (optional)" /></p>`}
71+
{(() => {
72+
let out = [];
73+
this.state.selected.forEach(l => {
74+
out.push(`
75+
<p>
76+
<input type="checkbox" name="l" value="${l.uuid}" id="${l.uuid.substr(0,5)}" />
77+
<label for="${l.uuid.substr(0,5)}">${l.name}</label>
78+
</p>`);
79+
});
80+
return out;
81+
})()}
82+
{`
83+
<p><input type="submit" value="Subscribe" /></p>
84+
</div>
85+
</form>
86+
`}
87+
88+
</pre>
89+
</Col>
90+
</Row>
91+
</section>
92+
)
93+
}
94+
}
95+
96+
export default Forms

frontend/src/Layout.js

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
1-
import React from "react";
2-
import { Switch, Route } from "react-router-dom";
3-
import { Link } from "react-router-dom";
4-
import { Layout, Menu, Icon } from "antd";
1+
import React from "react"
2+
import { Switch, Route } from "react-router-dom"
3+
import { Link } from "react-router-dom"
4+
import { Layout, Menu, Icon } from "antd"
55

6-
import logo from "./static/listmonk.svg";
6+
import logo from "./static/listmonk.svg"
77

88
// Views.
9-
import Dashboard from "./Dashboard";
10-
import Lists from "./Lists";
11-
import Subscribers from "./Subscribers";
12-
import Subscriber from "./Subscriber";
13-
import Templates from "./Templates";
14-
import Import from "./Import";
15-
import Campaigns from "./Campaigns";
16-
import Campaign from "./Campaign";
17-
import Media from "./Media";
9+
import Dashboard from "./Dashboard"
10+
import Lists from "./Lists"
11+
import Forms from "./Forms"
12+
import Subscribers from "./Subscribers"
13+
import Subscriber from "./Subscriber"
14+
import Templates from "./Templates"
15+
import Import from "./Import"
16+
import Campaigns from "./Campaigns"
17+
import Campaign from "./Campaign"
18+
import Media from "./Media"
1819

19-
const { Content, Footer, Sider } = Layout;
20-
const SubMenu = Menu.SubMenu;
21-
const year = new Date().getUTCFullYear();
20+
const { Content, Footer, Sider } = Layout
21+
const SubMenu = Menu.SubMenu
22+
const year = new Date().getUTCFullYear()
2223

2324
class Base extends React.Component {
2425
state = {
2526
basePath: "/" + window.location.pathname.split("/")[1],
2627
error: null,
2728
collapsed: false
28-
};
29+
}
2930

3031
onCollapse = collapsed => {
31-
this.setState({ collapsed });
32-
};
32+
this.setState({ collapsed })
33+
}
3334

3435
componentDidMount() {
3536
// For small screen devices collapse the menu by default.
3637
if (window.screen.width < 768) {
37-
this.setState({ collapsed: true });
38+
this.setState({ collapsed: true })
3839
}
39-
};
40+
}
4041

4142
render() {
4243
return (
@@ -65,12 +66,28 @@ class Base extends React.Component {
6566
<span>Dashboard</span>
6667
</Link>
6768
</Menu.Item>
68-
<Menu.Item key="/lists">
69-
<Link to="/lists">
70-
<Icon type="bars" />
71-
<span>Lists</span>
72-
</Link>
73-
</Menu.Item>
69+
<SubMenu
70+
key="/lists"
71+
title={
72+
<span>
73+
<Icon type="bars" />
74+
<span>Lists</span>
75+
</span>
76+
}
77+
>
78+
<Menu.Item key="/lists">
79+
<Link to="/lists">
80+
<Icon type="bars" />
81+
<span>All lists</span>
82+
</Link>
83+
</Menu.Item>
84+
<Menu.Item key="/lists/forms">
85+
<Link to="/lists/forms">
86+
<Icon type="form" />
87+
<span>Forms</span>
88+
</Link>
89+
</Menu.Item>
90+
</SubMenu>
7491
<SubMenu
7592
key="/subscribers"
7693
title={
@@ -146,6 +163,14 @@ class Base extends React.Component {
146163
<Lists {...{ ...this.props, route: props }} />
147164
)}
148165
/>
166+
<Route
167+
exact
168+
key="/lists/forms"
169+
path="/lists/forms"
170+
render={props => (
171+
<Forms {...{ ...this.props, route: props }} />
172+
)}
173+
/>
149174
<Route
150175
exact
151176
key="/subscribers"
@@ -230,8 +255,8 @@ class Base extends React.Component {
230255
>
231256
listmonk
232257
</a>{" "}
233-
&copy; 2019 {year !== 2019 ? " - " + year : ""}.
234-
Version { process.env.REACT_APP_VERSION } &mdash;{" "}
258+
&copy; 2019 {year !== 2019 ? " - " + year : ""}. Version{" "}
259+
{process.env.REACT_APP_VERSION} &mdash;{" "}
235260
<a
236261
href="https://listmonk.app/docs"
237262
target="_blank"
@@ -243,8 +268,8 @@ class Base extends React.Component {
243268
</Footer>
244269
</Layout>
245270
</Layout>
246-
);
271+
)
247272
}
248273
}
249274

250-
export default Base;
275+
export default Base

frontend/src/Lists.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ class CreateFormDef extends React.PureComponent {
9393
}
9494

9595
modalTitle(formType, record) {
96-
console.log(formType)
9796
if (formType === cs.FormCreate) {
9897
return "Create a list"
9998
}

frontend/src/index.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ hr {
4848
padding: 30px !important;
4949
}
5050

51+
ul.no {
52+
list-style-type: none;
53+
}
54+
ul.no li {
55+
margin-bottom: 10px;
56+
}
57+
5158
/* Layout */
5259
body {
5360
margin: 0;
@@ -94,6 +101,16 @@ section.content {
94101
}
95102

96103
/* Form */
104+
.list-form .html {
105+
background: #fafafa;
106+
padding: 30px;
107+
max-width: 100%;
108+
overflow-y: auto;
109+
max-height: 600px;
110+
}
111+
.list-form .lists label {
112+
display: block;
113+
}
97114

98115

99116
/* Table actions */

handlers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func registerHandlers(e *echo.Echo) {
9595
e.DELETE("/api/templates/:id", handleDeleteTemplate)
9696

9797
// Subscriber facing views.
98+
e.POST("/subscription/form", handleSubscriptionForm)
9899
e.GET("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
99100
"campUUID", "subUUID"))
100101
e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),

public.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"io"
99
"net/http"
1010
"strconv"
11+
"strings"
1112

1213
"github.com/knadh/listmonk/messenger"
1314
"github.com/knadh/listmonk/models"
15+
"github.com/knadh/listmonk/subimporter"
1416
"github.com/labstack/echo"
1517
"github.com/lib/pq"
1618
)
@@ -58,6 +60,11 @@ type msgTpl struct {
5860
Message string
5961
}
6062

63+
type subForm struct {
64+
subimporter.SubReq
65+
SubListUUIDs []string `form:"l"`
66+
}
67+
6168
var (
6269
pixelPNG = drawTransparentImage(3, 14)
6370
)
@@ -169,6 +176,47 @@ func handleOptinPage(c echo.Context) error {
169176
return c.Render(http.StatusOK, "optin", out)
170177
}
171178

179+
// handleOptinPage handles a double opt-in confirmation from subscribers.
180+
func handleSubscriptionForm(c echo.Context) error {
181+
var (
182+
app = c.Get("app").(*App)
183+
req subForm
184+
)
185+
186+
// Get and validate fields.
187+
if err := c.Bind(&req); err != nil {
188+
return err
189+
}
190+
191+
if len(req.SubListUUIDs) == 0 {
192+
return c.Render(http.StatusInternalServerError, "message",
193+
makeMsgTpl("Error", "",
194+
`No lists to subscribe to.`))
195+
}
196+
197+
// If there's no name, use the name bit from the e-mail.
198+
req.Email = strings.ToLower(req.Email)
199+
if req.Name == "" {
200+
req.Name = strings.Split(req.Email, "@")[0]
201+
}
202+
203+
// Validate fields.
204+
if err := subimporter.ValidateFields(req.SubReq); err != nil {
205+
return c.Render(http.StatusInternalServerError, "message",
206+
makeMsgTpl("Error", "", err.Error()))
207+
}
208+
209+
// Insert the subscriber into the DB.
210+
req.Status = models.SubscriberStatusEnabled
211+
req.ListUUIDs = pq.StringArray(req.SubListUUIDs)
212+
if _, err := insertSubscriber(req.SubReq, app); err != nil {
213+
return err
214+
}
215+
216+
return c.Render(http.StatusInternalServerError, "message",
217+
makeMsgTpl("Done", "", `Subscribed successfully.`))
218+
}
219+
172220
// handleLinkRedirect handles link UUID to real link redirection.
173221
func handleLinkRedirect(c echo.Context) error {
174222
var (

queries.sql

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,16 @@ WITH sub AS (
5454
VALUES($1, $2, $3, $4, $5)
5555
returning id
5656
),
57+
listIDs AS (
58+
SELECT id FROM lists WHERE
59+
(CASE WHEN ARRAY_LENGTH($6::INT[], 1) > 0 THEN id=ANY($6)
60+
ELSE uuid=ANY($7::UUID[]) END)
61+
),
5762
subs AS (
5863
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
5964
VALUES(
6065
(SELECT id FROM sub),
61-
UNNEST($6::INT[]),
66+
UNNEST(ARRAY(SELECT id FROM listIDs)),
6267
(CASE WHEN $4='blacklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
6368
)
6469
ON CONFLICT (subscriber_id, list_id) DO UPDATE
@@ -302,7 +307,7 @@ SELECT COUNT(*) OVER () AS total, lists.*, COUNT(subscriber_lists.subscriber_id)
302307

303308
-- name: get-lists-by-optin
304309
-- Can have a list of IDs or a list of UUIDs.
305-
SELECT * FROM lists WHERE optin=$1::list_optin AND
310+
SELECT * FROM lists WHERE (CASE WHEN $1 != '' THEN optin=$1::list_optin ELSE TRUE END) AND
306311
(CASE WHEN $2::INT[] IS NOT NULL THEN id = ANY($2::INT[])
307312
WHEN $3::UUID[] IS NOT NULL THEN uuid = ANY($3::UUID[])
308313
END) ORDER BY name;

0 commit comments

Comments
 (0)