add embedly and newsletter packages

This commit is contained in:
Sacha Greif 2014-08-29 10:37:35 +09:00
parent 8ae358181e
commit 733f367f37
29 changed files with 1435 additions and 43 deletions

44
packages/.gitignore vendored
View file

@ -1,44 +1,2 @@
router
momentjs
database-forms
paginated-subscription
crypto-md5
page-js-ie-support
HTML5-History-API
crypto-base
iron-router
nprogress
i18next-meteor
/momentjs
/crypto-base
/database-forms
/crypto-md5
/iron-router
/nprogress
/fast-render
/spin
/blaze-layout
/autoform
/simple-schema
/collection2
/moment
/iron-router-progress
/jquery-hotkeys
/marked
/bootstrap3-datepicker
/subs-manager
/telescope-module-embedly
/telescope-recently-commented
/iron-layout
/iron-core
/iron-dynamic-template
/handlebars-server
/npm
/mailchimp
/synced-cron
/cookies
/telescope-newsletter
/accounts-entry
/accounts-t9n
/simple-form
/underscore-string-latest
/npm-container

View file

@ -0,0 +1,7 @@
### v0.2.9
- Update to Meteor 0.9.0.
### v0.2.8
- Don't display image if it cannot be found.

View file

@ -0,0 +1,12 @@
The Telescope [Embedly](http://embed.ly) module.
More details in [this blog post](http://telesc.pe/blog/extending-telescope-with-embedly-thumbnails/).
### Install
1. `mrt add telescope-module-embedly`.
2. Go to the Telescope settings page and add your Embedly API key.
### Extra Features
When linking to videos, the thumbnail becomes clickable and opens the video in a lightbox.

View file

@ -0,0 +1,79 @@
.post-thumbnail{
margin-right: 0px !important;
}
.post-thumbnail-link{
display: inline-block;
margin-right: 10px;
position: relative;
overflow: hidden;
max-width: 80px;
max-height: 50px;
}
.post-thumbnail-image{
width: 100%;
height: 100%;
display: block;
}
.post-thumbnail-has-video:after{
content: '‣';
font-size: 40px;
position: absolute;
top: 50%;
left: 50%;
height: 36px;
width: 36px;
margin: -18px 0 0 -18px;
line-height: 32px;
text-align: center;
color: white;
display: block;
z-index: 10;
text-shadow: 0px 1px 2px rgba(0,0,0,0.5);
background: rgba(0,0,0,0.4);
border: 2px solid rgba(255,255,255,0.8);
border-radius: 100%;
}
@media screen and (max-width: 30em) {
.post-thumbnail{
float: right;
margin: 10px;
}
}
.post-video-lightbox{
position: fixed;
background: rgba(0,0,0,0.7);
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 10000;
}
.post-video-lightbox-inner{
position: absolute;
top: 50%;
left: 50%;
-webkit-transform:translate(-50%, -50%);
-moz-transform:translate(-50%, -50%);
transform: translate(-50%, -50%);
z-index: 20000;
}
.post-video-lightbox-hide, .post-video-lightbox-hide:link, .post-video-lightbox-hide:visited, .post-video-lightbox-hide:hover{
display: block;
position: absolute;
top: 20px;
right: 20px;
height: 40px;
width: 40px;
color: white;
font-size: 40px;
text-align: center;
line-height: 40px;
}

View file

@ -0,0 +1,10 @@
<template name="postThumbnail">
{{#if thumbnailUrl}}
<a class="post-thumbnail-link {{playVideoClass}}" href="{{postLink}}" target="_blank">
<img class="post-thumbnail-image" src="{{thumbnailUrl}}" onerror="this.style.display='none';"/>
</a>
{{/if}}
{{#if media}}
{{> UI.dynamic template=videoTemplate data=this}}
{{/if}}
</template>

View file

@ -0,0 +1,19 @@
Template[getTemplate('postThumbnail')].helpers({
postLink: function () {
return !!this.url ? getOutgoingUrl(this.url) : "/posts/"+this._id;
},
playVideoClass: function () {
return !!this.media ? 'post-thumbnail-has-video': '';
},
videoTemplate: function () {
return getTemplate('postVideo');
}
});
Template[getTemplate('postThumbnail')].events({
'click .post-thumbnail-has-video': function (e) {
e.preventDefault();
$('body').addClass('showing-lightbox');
$(e.target).parents('.post').find('.post-video-lightbox').fadeIn('fast');
}
})

View file

@ -0,0 +1,10 @@
<template name="postVideo">
{{#with media}}
<div class="post-video-lightbox hidden">
<a class="post-video-lightbox-hide" href="#">×</a>
<div class="post-video-lightbox-inner">
{{{html}}}
</div>
</div>
{{/with}}
</template>

View file

@ -0,0 +1,7 @@
Template[getTemplate('postVideo')].events({
'click .post-video-lightbox-hide, click .post-video-lightbox': function (e) {
e.preventDefault();
$(e.target).parents('.post').find('.post-video-lightbox').fadeOut('fast');
$('body').removeClass('showing-lightbox');
}
})

View file

@ -0,0 +1,33 @@
var thumbnailProperty = {
propertyName: 'thumbnailUrl',
propertySchema: {
type: String,
optional: true
}
}
addToPostSchema.push(thumbnailProperty);
var mediaProperty = {
propertyName: 'media',
propertySchema: {
type: Object,
optional: true,
blackbox: true
}
}
addToPostSchema.push(mediaProperty);
postModules.push({
template: 'postThumbnail',
position: 'center-left'
});
var embedlyKeyProperty = {
propertyName: 'embedlyKey',
propertySchema: {
type: String,
optional: true
}
}
addToSettingsSchema.push(embedlyKeyProperty);

View file

@ -0,0 +1,65 @@
getEmbedlyData = function (url) {
var data = {}
var extractBase = 'http://api.embed.ly/1/extract';
var embedlyKey = getSetting('embedlyKey');
try {
if(!embedlyKey)
throw new Error("Couldn't find an Embedly API key! Please add it to your Telescope settings.")
var result = Meteor.http.get(extractBase, {
params: {
key: embedlyKey,
url: url,
image_width: 200,
image_height: 150,
image_method: 'crop'
}
});
if(!result.data.images.length)
throw new Error("Couldn't find an image!");
data.thumbnailUrl = result.data.images[0].url;
if(typeof result.data.media !== 'undefined')
data.media = result.data.media
return data;
} catch (error) {
console.log(error)
return null;
}
}
Meteor.methods({
testGetEmbedlyData: function (url) {
console.log(getEmbedlyData(url))
},
setThumbnail: function (post) {
var set = {};
if(post.url){
var data = getEmbedlyData(post.url);
if(!!data && !!data.thumbnailUrl)
set.thumbnailUrl = data.thumbnailUrl;
if(!!data && !!data.media.html)
set.media = data.media
console.log(set)
Posts.update({_id: post._id}, {$set: set});
}
}
});
var extendPost = function (post) {
if(post.url){
var data = getEmbedlyData(post.url);
if(!!data && !!data.thumbnailUrl)
post.thumbnailUrl = data.thumbnailUrl;
if(!!data && !!data.media.html)
post.media = data.media
}
return post;
}
postSubmitServerCallbacks.push(extendPost);

View file

@ -0,0 +1,29 @@
Package.describe({
summary: "Telescope Embedly module package",
version: '0.2.9',
name: "telescope-module-embedly",
git: 'https://github.com/TelescopeJS/Telescope-Module-Embedly.git'
});
Package.onUse( function(api) {
api.versionsFrom("METEOR@0.9.0");
api.use(['telescope-lib', 'telescope-base'], ['client', 'server']);
api.use(['http'], ['server']);
api.use(['templating'], ['client']);
api.add_files(['lib/embedly.js'], ['client', 'server']);
api.add_files(['lib/server/get_embedly_data.js'], ['server']);
api.add_files([
'lib/client/post_thumbnail.html',
'lib/client/post_thumbnail.js',
'lib/client/post_thumbnail.css',
'lib/client/post_video.html',
'lib/client/post_video.js'
], ['client']);
});

View file

@ -0,0 +1,8 @@
{
"name": "telescope-module-embedly",
"description": "The Telescope Embedly module.",
"homepage": "https://github.com/TelescopeJS/telescope-module-embedly",
"author": "Sacha Greif <info@sachagreif.com> (http://sachagreif.com)",
"version": "0.2.8",
"git": "https://github.com/TelescopeJS/telescope-module-embedly.git"
}

View file

@ -0,0 +1,87 @@
{
"dependencies": [
[
"blaze",
"1.0.3"
],
[
"deps",
"1.0.1"
],
[
"ejson",
"1.0.0"
],
[
"geojson-utils",
"1.0.0"
],
[
"htmljs",
"1.0.0"
],
[
"http",
"1.0.3"
],
[
"id-map",
"1.0.0"
],
[
"jquery",
"1.0.0"
],
[
"json",
"1.0.0"
],
[
"meteor",
"1.0.2"
],
[
"minimongo",
"1.0.1"
],
[
"observe-sequence",
"1.0.1"
],
[
"ordered-dict",
"1.0.0"
],
[
"random",
"1.0.0"
],
[
"telescope-base",
"0.0.0"
],
[
"telescope-i18n",
"0.0.0"
],
[
"telescope-lib",
"0.2.9"
],
[
"templating",
"1.0.4"
],
[
"ui",
"1.0.0"
],
[
"underscore",
"1.0.0"
]
],
"pluginDependencies": [],
"toolVersion": "meteor-tool@1.0.26",
"format": "1.0"
}

View file

@ -0,0 +1,8 @@
## v0.1.6
- Return error message or campaign subject for cron job.
- Change Newsletter Frequency option to a `select` input.
## v0.1.5
- Added option to show/hide newsletter sign-up banner

View file

@ -0,0 +1,50 @@
# Telescope Newsletter
This package schedules an automatic newsletter digest.
![Newsletter](http://f.cl.ly/items/0V0F351k1R1i3L1k1D0J/telescope-newsletter.png)
### Install
1. `mrt add telescope-newsletter`.
2. Go to the Telescope settings page and add your MailChimp API key and List ID.
### Dependencies
- [meteor-mailchimp](https://github.com/MiroHibler/meteor-mailchimp/)
- [synced-cron](https://github.com/percolatestudio/meteor-synced-cron)
- [handlebars-server](https://github.com/EventedMind/meteor-handlebars-server)
- [meteor-npm](https://github.com/arunoda/meteor-npm/)
### Settings
- **Show Banner**:
- **MailChimp API Key**:
- **MailChimp List ID**:
- **Newsletter Frequency**: Choose from every day, three times a week, and once a week. Note that changes to this setting require you to restart your app to take effect.
- **Posts Per Newsletter**: how many posts each newsletter should contain.
Note that for this package to work properly, you'll also need to fill in the **Default Email** setting.
### How It Works
The package works with [MailChimp](http://mailchimp.com), which means you'll need to fill in an API key and List ID in your Telescope app's settings panel.
Every `x` days, it builds a digest consisting of the top `y` items posted in the past `x` days that haven't yet been sent out in a newsletter.
It then creates a campaign in MailChimp and schedules it to be sent out **one hour later**, and sends you a confirmation email (to give you some time to check that everything looks good).
### Test Routes
If you want to preview your email templates, you can do so at the following routes:
- **Digest**: [http://localhost:3000/email/campaign](http://localhost:3000/email/campaign)
- **Confirmation**: [http://localhost:3000/email/digest-confirmation](http://localhost:3000/email/digest-confirmation)
(Replace `http://localhost:3000` with your app's URL)
### Newsletter Sign-Up Banner
This package also includes a newsletter sign-up banner that uses the MailChimp API to add people to your list.
![Newsletter Banner](http://f.cl.ly/items/3k282w2b0I1U3y200944/telescope-newsletter-banner.png)

View file

@ -0,0 +1,102 @@
.newsletter-banner.newsletter-banner{
background: rgba(255,255,255,0.7);
margin-bottom: 10px;
padding: 20px;
position: relative;
overflow: hidden;
}
.newsletter-banner form{
text-align: center;
vertical-align: middle;
}
.newsletter-tagline{
font-weight: bold;
margin-right: 5px;
display: inline-block;
vertical-align: middle;
}
.newsletter-subscribed{
text-align: center;
font-weight: bold;
vertical-align: middle;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
opacity: 0;
line-height: 74px;
pointer-events: none;
}
.newsletter-banner form, .newsletter-subscribed{
-webkit-transition:opacity 0.3s ease-out;
-moz-transition:opacity 0.3s ease-out;
-o-transition:opacity 0.3s ease-out;
transition:opacity 0.3s ease-out;
}
.newsletter-email.newsletter-email{
display: inline-block;
margin-right: 2px;
width: auto;
vertical-align: middle;
border-color: rgba(0,0,0,0.5);
border-radius: 3px;
}
.button.newsletter-button{
display: inline-block;
float: none;
vertical-align: middle;
}
.show-loader{
pointer-events:none;
}
.show-loader .newsletter-tagline, .show-loader .newsletter-email{
opacity: 0.35;
}
.show-loader .newsletter-button{
color: rgba(255,255,255,0); /* hide button text */
}
.button-loader{
display: none;
}
.show-loader .button-loader{
position: absolute;
z-index: 100;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
display: block;
}
.newsletter-dismiss, .newsletter-dismiss:link, .newsletter-dismiss:visited{
display: block;
position: absolute;
height: 30px;
width: 30px;
border-radius: 100%;
background: rgba(0,0,0,0.15);
color: white;
top: 50%;
margin-top: -15px;
right: 20px;
font-size: 24px;
text-align: center;
vertical-align: middle;
line-height: 26px;
}
.newsletter-dismiss:hover{
color: inherit;
}
@media screen and (max-width: 30em) {
.newsletter-dismiss, .newsletter-dismiss:link{
top: 10px;
right: 10px;
margin-top: 0px;
}
.newsletter-tagline{
padding: 0 30px;
}
.newsletter-tagline, .newsletter-email, .newsletter-button{
margin-bottom: 10px;
}
}

View file

@ -0,0 +1,15 @@
<template name="newsletterBanner">
{{#if showBanner}}
<div class="newsletter-banner grid-module grid">
<form>
<h4 class="newsletter-tagline">Receive the best of {{siteName}} right in your inbox</h4>
{{#if isNotConnected}}
<input class="newsletter-email" type="email" placeholder="Your Email">
{{/if}}
<button class="button newsletter-button">Get Newsletter<span class="button-loader"><img src="/img/loading-balls.svg"/></span></button>
</form>
<h4 class="newsletter-subscribed">Thanks for subscribing!</h4>
<a href="#" class="newsletter-dismiss">×</a>
</div>
{{/if}}
</template>

View file

@ -0,0 +1,87 @@
var confirmSubscription = function () {
$('.newsletter-banner form').css('opacity', 0);
$('.newsletter-banner .newsletter-subscribed').css('display', 'block').css('opacity', 1);
Meteor.setInterval(function () {
// required because otherwise banner disappears immediately after confirmation
dismissBanner();
}, 2000)
}
var dismissBanner = function () {
$('.newsletter-banner').fadeOut('fast', function () {
if(Meteor.user()){
// if user is connected, change setting in their account
setUserSetting('showBanner', false);
}else{
// set cookie
Cookie.set('showBanner', "no");
}
});
}
Template[getTemplate('newsletterBanner')].helpers({
siteName: function () {
return getSetting('title');
},
isNotConnected: function () {
return !Meteor.user()
},
showBanner: function () {
// note: should not be reactive
if(
getSetting('showBanner', false) == false
|| Router.current().path != '/'
|| Cookie.get('showBanner') == "no"
|| (Meteor.user() && getUserSetting('showBanner', true) == false)
|| (Meteor.user() && getUserSetting('subscribedToNewsletter', false) == true)
){
return false;
}else{
return true;
}
}
});
Template[getTemplate('newsletterBanner')].events({
'click .newsletter-button': function (e) {
e.preventDefault();
var $banner = $('.newsletter-banner');
if(Meteor.user()){
$banner.addClass('show-loader');
Meteor.call('addCurrentUserToMailChimpList', function (error, result) {
$banner.removeClass('show-loader');
if(error){
console.log(error);
throwError(error.message);
}else{
console.log(result);
confirmSubscription();
}
});
}else{
var email = $('.newsletter-email').val();
if(!email){
alert('Please fill in your email.');
return
}
$banner.addClass('show-loader');
Meteor.call('addEmailToMailChimpList', email, function (error, result) {
$banner.removeClass('show-loader');
if(error){
console.log(error);
throwError(error.message);
}else{
console.log(result);
confirmSubscription();
}
});
}
// $('body').addClass('showing-lightbox');
// $(e.target).parents('.post').find('.post-video-lightbox').fadeIn('fast');
},
'click .newsletter-dismiss': function (e) {
$('.newsletter-banner').fadeOut('fast');
dismissBanner();
e.preventDefault();
}
})

View file

@ -0,0 +1,132 @@
campaignSchema = new SimpleSchema({
_id: {
type: String,
optional: true
},
createdAt: {
type: Date,
optional: true
},
sentAt: {
type: String,
optional: true
},
status: {
type: String,
optional: true
},
posts: {
type: [String],
optional: true
},
webHits: {
type: Number,
optional: true
},
});
Campaigns = new Meteor.Collection("campaigns", {
schema: campaignSchema
});
addToPostSchema.push(
{
propertyName: 'scheduledAt',
propertySchema: {
type: Date,
optional: true
}
}
);
// Settings
// note for next two fields: need to add a way to tell app not to publish field to client except for admins
var showBanner = {
propertyName: 'showBanner',
propertySchema: {
type: Boolean,
optional: true,
label: 'Show newsletter sign-up banner'
}
}
addToSettingsSchema.push(showBanner);
var mailChimpAPIKey = {
propertyName: 'mailChimpAPIKey',
propertySchema: {
type: String,
optional: true,
}
}
addToSettingsSchema.push(mailChimpAPIKey);
var mailChimpListId = {
propertyName: 'mailChimpListId',
propertySchema: {
type: String,
optional: true,
}
}
addToSettingsSchema.push(mailChimpListId);
var postsPerNewsletter = {
propertyName: 'postsPerNewsletter',
propertySchema: {
type: Number,
optional: true
}
}
addToSettingsSchema.push(postsPerNewsletter);
var newsletterFrequency = {
propertyName: 'newsletterFrequency',
propertySchema: {
type: Number,
optional: true,
autoform: {
options: [
{
value: 1,
label: 'Every Day'
},
{
value: 2,
label: 'Mondays, Wednesdays, Fridays'
},
{
value: 3,
label: 'Mondays & Thursdays'
},
{
value: 7,
label: 'Once a week (Mondays)'
},
{
value: 0,
label: "Don't send newsletter"
}
]
},
label: 'Newsletter Frequency (requires restart)'
}
}
addToSettingsSchema.push(newsletterFrequency);
// create new "campaign" lens for all posts from the past X days that haven't been scheduled yet
viewParameters.campaign = function (terms) {
return {
find: {
scheduledAt: {$exists: false},
postedAt: {
$gte: terms.after
}
},
options: {sort: {sticky: -1, score: -1}}
};
}
heroModules.push({
template: 'newsletterBanner'
});

View file

@ -0,0 +1,85 @@
defaultFrequency = 7;
defaultPosts = 5;
getCampaignPosts = function (postsCount) {
var newsletterFrequency = getSetting('newsletterFrequency', defaultFrequency);
// look for last scheduled campaign in the database
var lastCampaign = SyncedCron._collection.findOne({name: 'Schedule newsletter'}, {sort: {finishedAt: -1}, limit: 1});
// if there is a last campaign use its date, else default to posts from the last 7 days
var lastWeek = moment().subtract('days', 7).toDate();
var after = (typeof lastCampaign != 'undefined') ? lastCampaign.finishedAt : lastWeek
var params = getParameters({
view: 'campaign',
limit: postsCount,
after: after
});
return Posts.find(params.find, params.options).fetch();
}
buildCampaign = function (postsArray) {
var postsHTML = '', subject = '';
// 1. Iterate through posts and pass each of them through a handlebars template
postsArray.forEach(function (post, index) {
if(index > 0)
subject += ', ';
subject += post.title;
var postUser = Meteor.users.findOne(post.userId);
// the naked post object as stored in the database is missing a few properties, so let's add them
var properties = _.extend(post, {
authorName: getAuthorName(post),
postLink: getPostLink(post),
profileUrl: getProfileUrl(postUser),
postPageLink: getPostPageUrl(post),
date: moment(post.postedAt).format("MMMM D YYYY")
});
if (post.body)
properties.body = marked(trimWords(post.body, 20)).replace('<p>', '').replace('</p>', ''); // remove p tags
if(post.url)
properties.domain = getDomain(post.url)
postsHTML += getEmailTemplate('emailPostItem')(properties);
});
// 2. Wrap posts HTML in digest template
var digestHTML = getEmailTemplate('emailDigest')({
siteName: getSetting('title'),
date: moment().format("dddd, MMMM Do YYYY"),
content: postsHTML
});
// 3. wrap digest HTML in email wrapper tempalte
var emailHTML = buildEmailTemplate(digestHTML);
return {
postIds: _.pluck(postsArray, '_id'),
subject: trimWords(subject, 15),
html: emailHTML
}
}
scheduleNextCampaign = function () {
var posts = getCampaignPosts(getSetting('postsPerNewsletter', defaultPosts));
if(!!posts.length){
return scheduleCampaign(buildCampaign(posts))
}else{
var result = 'No posts to schedule today…';
console.log(result)
return result
}
}
Meteor.methods({
testCampaign: function () {
scheduleNextCampaign();
}
});

View file

@ -0,0 +1,74 @@
Later = Npm.require('later');
defaultFrequency = 7; // once a week
getSchedule = function (parser) {
var frequency = getSetting('newsletterFrequency', defaultFrequency);
switch (frequency) {
case 1: // every day
// sched = {schedules: [{dw: [1,2,3,4,5,6,0]}]};
return parser.recur().on(1,2,3,4,5,6,0).dayOfWeek();
case 2: // Mondays, Wednesdays, Fridays
// sched = {schedules: [{dw: [2,4,6]}]};
return parser.recur().on(2,4,6).dayOfWeek();
case 3: // Mondays, Thursdays
// sched = {schedules: [{dw: [2,5]}]};
return parser.recur().on(2,5).dayOfWeek();
case 7: // Once a week (Mondays)
// sched = {schedules: [{dw: [2]}]};
return parser.recur().on(2).dayOfWeek();
default: // Don't send
return null;
}
}
SyncedCron.getNext = function (jobName) {
var scheduledAt;
try {
this._entries.some(function(entry) {
if(entry.name === jobName){
var schedule = entry.schedule(Later.parse);
scheduledAt = Later.schedule(schedule).next(1);
return true;
}
});
}
catch (error) {
console.log(error)
scheduledAt = 'No job scheduled';
}
return scheduledAt;
}
getNextCampaignSchedule = function () {
return SyncedCron.getNext('Schedule newsletter');
}
SyncedCron.add({
name: 'Schedule newsletter',
schedule: function(parser) {
// parser is a later.parse object
// var sched;
return getSchedule(parser)
},
job: function() {
scheduleNextCampaign();
}
});
Meteor.startup(function() {
if(getSetting('newsletterFrequency', defaultFrequency) != 0) {
SyncedCron.start();
};
});
Meteor.methods({
getNextJob: function (jobName) {
return SyncedCron.getNext(jobName);
}
});

View file

@ -0,0 +1,141 @@
scheduleCampaign = function (campaign) {
MailChimpOptions.apiKey = getSetting('mailChimpAPIKey');
MailChimpOptions.listId = getSetting('mailChimpListId');
var htmlToText = Meteor.require('html-to-text');
var text = htmlToText.fromString(campaign.html, {
wordwrap: 130
});
var defaultEmail = getSetting('defaultEmail');
var result= '';
if(!!MailChimpOptions.apiKey && !!MailChimpOptions.listId){
console.log( 'Creating campaign…');
try {
var api = new MailChimp();
} catch ( error ) {
console.log( error.message );
}
api.call( 'campaigns', 'create', {
type: 'regular',
options: {
list_id: MailChimpOptions.listId,
subject: campaign.subject,
from_email: getSetting('defaultEmail'),
from_name: getSetting('title')+ ' Top Posts',
},
content: {
html: campaign.html,
text: text
}
}, Meteor.bindEnvironment(function ( error, result ) {
if ( error ) {
console.log( error.message );
result = error.message;
} else {
console.log( 'Campaign created');
// console.log( JSON.stringify( result ) );
var cid = result.id;
var archive_url = result.archive_url;
var scheduledTime = moment().zone(0).add('hours', 1).format("YYYY-MM-DD HH:mm:ss");
api.call('campaigns', 'schedule', {
cid: cid,
schedule_time: scheduledTime
}, Meteor.bindEnvironment(function ( error, result) {
if (error) {
console.log( error.message );
result = error.message;
}else{
console.log('Campaign scheduled for '+scheduledTime);
console.log(campaign.subject)
// console.log( JSON.stringify( result ) );
// mark posts as sent
Posts.update({_id: {$in: campaign.postIds}}, {$set: {scheduledAt: new Date()}}, {multi: true})
// send confirmation email
var confirmationHtml = getEmailTemplate('emailDigestConfirmation')({
time: scheduledTime,
newsletterLink: archive_url,
subject: campaign.subject
});
sendEmail(defaultEmail, 'Newsletter scheduled', buildEmailTemplate(confirmationHtml));
result = campaign.subject;
}
}));
}
}));
}
return result;
}
addToMailChimpList = function(userOrEmail, confirm, done){
var user, email;
if(typeof userOrEmail == "string"){
user = null;
email = userOrEmail;
}else if(typeof userOrEmail == "object"){
user = userOrEmail;
email = getEmail(user);
if (!email)
throw 'User must have an email address';
}
MailChimpOptions.apiKey = getSetting('mailChimpAPIKey');
MailChimpOptions.listId = getSetting('mailChimpListId');
// add a user to a MailChimp list.
// called when a new user is created, or when an existing user fills in their email
if(!!MailChimpOptions.apiKey && !!MailChimpOptions.listId){
console.log('adding "'+email+'" to MailChimp list…');
try {
var api = new MailChimp();
} catch ( error ) {
console.log( error.message );
}
api.call( 'lists', 'subscribe', {
id: MailChimpOptions.listId,
email: {"email": email},
double_optin: confirm
}, Meteor.bindEnvironment(function ( error, result ) {
if ( error ) {
console.log( error.message );
done(error, null);
} else {
console.log( JSON.stringify( result ) );
if(!!user)
setUserSetting('subscribedToNewsletter', true, user);
done(null, result);
}
}));
}
};
syncAddToMailChimpList = Async.wrap(addToMailChimpList);
Meteor.methods({
addCurrentUserToMailChimpList: function(){
var currentUser = Meteor.users.findOne(this.userId);
try {
return syncAddToMailChimpList(currentUser, false);
} catch (error) {
throw new Meteor.Error(500, error.message);
}
},
addEmailToMailChimpList: function (email) {
try {
return syncAddToMailChimpList(email, true);
} catch (error) {
throw new Meteor.Error(500, error.message);
}
}
})

View file

@ -0,0 +1,35 @@
Meteor.startup(function () {
Router.map(function() {
this.route('campaign', {
where: 'server',
path: '/email/campaign',
action: function() {
var campaign = buildCampaign(getCampaignPosts(getSetting('postsPerNewsletter', 5)));
var campaignSubject = '<div class="campaign-subject"><strong>Subject:</strong> '+campaign.subject+' (note: contents might change)</div>';
var campaignSchedule = '<div class="campaign-schedule"><strong>Scheduled for:</strong> '+getNextCampaignSchedule()+'</div>';
this.response.write(campaignSubject+campaignSchedule+campaign.html);
this.response.end();
}
});
this.route('digestConfirmation', {
where: 'server',
path: '/email/digest-confirmation',
action: function() {
var confirmationHtml = getEmailTemplate('emailDigestConfirmation')({
time: 'January 1st, 1901',
newsletterLink: 'http://example.com',
subject: 'Lorem ipsum dolor sit amet'
});
this.response.write(buildEmailTemplate(confirmationHtml));
this.response.end();
}
});
});
});

View file

@ -0,0 +1,48 @@
<style type="text/css">
.email-digest{
}
.digest-date{
color: #999;
font-weight: normal;
font-size: 16px;
}
.post-item{
border-top: 1px solid #ddd;
}
.post-date{
font-size: 13px;
color: #999;
}
.post-title{
font-size: 18px;
line-height: 1.6;
}
.post-thumbnail{
}
.post-meta{
font-size: 13px;
color: #999;
margin: 5px 0;
}
.post-meta a{
color: #333;
}
.post-domain{
font-weight: bold;
}
.post-body-excerpt{
font-size: 14px;
}
.post-body-excerpt p{
margin: 0;
}
</style>
<span class="heading">Recently on {{siteName}}</span>
<span class="digest-date"> {{date}}</span>
<br><br>
<div class="email-digest">
{{{content}}}
</div>
<br>

View file

@ -0,0 +1,3 @@
<span class="heading">Newsletter scheduled for {{time}}</span><br><br>
<a href="{{newsletterLink}}">{{subject}}</a><br><br>

View file

@ -0,0 +1,34 @@
<div class="post-item">
<br >
<span class="post-title">
{{#if thumbnailUrl}}
<img class="post-thumbnail" src="{{thumbnailUrl}}"/>&nbsp;
{{/if}}
<a href="{{postLink}}" target="_blank">{{title}}</a>
</span>
<div class="post-meta">
{{#if domain}}
<a class="post-domain" href="">{{domain}}</a>
|
{{/if}}
<span class="post-submitted">Submitted by <a href="{{profileUrl}}" class="comment-link" target="_blank">{{authorName}}</a></span>
<span class="post-date">on {{date}}</span>
|
<a href="{{postPageLink}}" class="comment-link" target="_blank">{{comments}} Comments</a>
</div>
{{#if body}}
<div class="post-body-excerpt">
{{{body}}}
<a href="{{postPageLink}}" class="comment-link" target="_blank">Read more</a>
</div>
{{/if}}
<br>
</div>

View file

@ -0,0 +1,51 @@
Package.describe({summary: "Telescope email digest package"});
Npm.depends({later: "1.1.6"});
Package.onUse(function (api) {
api.use([
'telescope-lib',
'telescope-base',
'aldeed:simple-schema',
'mrt:mailchimp'
], ['client', 'server']);
api.use([
'jquery',
'underscore',
'iron:router',
'templating',
'mrt:cookies'
], 'client');
api.use([
'percolatestudio:synced-cron',
'cmather:handlebars-server',
'meteorhacks:npm'
], ['server']);
api.add_files([
'lib/newsletter.js'
], ['client', 'server']);
api.add_files([
'lib/client/newsletter_banner.html',
'lib/client/newsletter_banner.js',
'lib/client/newsletter_banner.css'
], ['client']);
api.add_files([
'lib/server/campaign.js',
'lib/server/cron.js',
'lib/server/mailchimp.js',
'lib/server/routes.js',
'lib/server/templates/emailDigest.handlebars',
'lib/server/templates/emailDigestConfirmation.handlebars',
'lib/server/templates/emailPostItem.handlebars'
], ['server']);
api.export([
]);
});

View file

@ -0,0 +1,8 @@
{
"name": "telescope-newsletter",
"description": "Telescope Newsletter package.",
"homepage": "https://github.com/TelescopeJS/telescope-newsletter",
"author": "Sacha Greif <info@sachagreif.com> (http://sachagreif.com)",
"version": "0.1.9",
"git": "https://github.com/TelescopeJS/telescope-newsletter.git"
}

View file

@ -0,0 +1,195 @@
{
"dependencies": [
[
"aldeed:simple-schema",
"0.7.0"
],
[
"application-configuration",
"1.0.0"
],
[
"binary-heap",
"1.0.0"
],
[
"blaze",
"1.0.3"
],
[
"blaze-tools",
"1.0.0"
],
[
"callback-hook",
"1.0.0"
],
[
"check",
"1.0.0"
],
[
"cmather:handlebars-server",
"0.2.0"
],
[
"deps",
"1.0.1"
],
[
"ejson",
"1.0.0"
],
[
"follower-livedata",
"1.0.0"
],
[
"geojson-utils",
"1.0.0"
],
[
"handlebars",
"1.0.0"
],
[
"html-tools",
"1.0.0"
],
[
"htmljs",
"1.0.0"
],
[
"id-map",
"1.0.0"
],
[
"iron:core",
"0.3.2"
],
[
"iron:dynamic-template",
"0.3.0"
],
[
"iron:layout",
"0.3.0"
],
[
"iron:router",
"0.9.1"
],
[
"jquery",
"1.0.0"
],
[
"json",
"1.0.0"
],
[
"livedata",
"1.0.7"
],
[
"logging",
"1.0.2"
],
[
"meteor",
"1.0.2"
],
[
"meteorhacks:npm",
"1.1.2"
],
[
"minifiers",
"1.0.2"
],
[
"minimongo",
"1.0.1"
],
[
"mongo-livedata",
"1.0.3"
],
[
"mrt:cookies",
"0.3.0"
],
[
"mrt:mailchimp",
"0.4.0"
],
[
"observe-sequence",
"1.0.1"
],
[
"ordered-dict",
"1.0.0"
],
[
"percolatestudio:synced-cron",
"0.1.1"
],
[
"random",
"1.0.0"
],
[
"reactive-dict",
"1.0.0"
],
[
"retry",
"1.0.0"
],
[
"routepolicy",
"1.0.0"
],
[
"spacebars",
"1.0.0"
],
[
"spacebars-compiler",
"1.0.1"
],
[
"telescope-base",
"0.0.0"
],
[
"telescope-i18n",
"0.0.0"
],
[
"telescope-lib",
"0.2.9"
],
[
"templating",
"1.0.4"
],
[
"ui",
"1.0.0"
],
[
"underscore",
"1.0.0"
],
[
"webapp",
"1.0.2"
]
],
"pluginDependencies": [],
"toolVersion": "meteor-tool@1.0.26",
"format": "1.0"
}