Adding lazy-load version of feed component to load data using AJAX after page content has been rendered.

This commit is contained in:
Jason Williams
2020-10-23 22:54:30 +00:00
parent 2a8454d197
commit af4f1366a0
71 changed files with 1753 additions and 88 deletions

View File

@@ -0,0 +1,39 @@
<?php namespace jasonwilliams\feed;
use System\Classes\PluginBase;
use JasonWilliams\Feed\Classes\SocialApis;
class Plugin extends PluginBase
{
public function registerSettings()
{
return [
'settings' => [
'label' => 'Feed',
'description' => 'Manage API keys and settings related to the feed plugin',
'icon' => 'icon-newspaper-o',
'class' => 'JasonWilliams\Feed\Models\Settings',
'order' => 500,
]
];
}
public function registerComponents()
{
return [
'\JasonWilliams\Feed\Components\ShortFeed' => 'ShortFeed',
'\JasonWilliams\Feed\Components\LazyLoadShortFeed' => 'LazyLoadShortFeed',
'\JasonWilliams\Feed\Components\ChannelList' => 'ChannelList',
'\JasonWilliams\Feed\Components\TagList' => 'TagList'
];
}
public function registerSchedule($schedule)
{
$schedule->call(function() {
SocialApis::updateTwitter();
SocialApis::updateInstagram();
SocialApis::updateFoursquare();
})->everyThirtyMinutes();
}
}

View File

@@ -0,0 +1,10 @@
$(() => {
var obj = {};
if (typeof renderPartial !== 'string') obj = { '@feed': '#feedarea' }
else obj[renderPartial] = '#feedarea'
$.request('LazyLoadShortFeed::onUpdateRequested', {
update: obj,
complete: () => { $(window).trigger('feedLoaded') }
})
})

View File

@@ -0,0 +1,287 @@
<?php namespace JasonWilliams\Feed\Classes;
use JasonWilliams\Feed\Models\Settings;
use JasonWilliams\Feed\Models\FeedItem;
use October\Rain\Network\Http;
class SocialApis
{
// Update the database with the latest tweets
public static function updateTwitter()
{
$tweets = self::fetchTwitter();
if (sizeof($tweets) > 0)
{
foreach ($tweets as $tweet)
{
// Skip tweets created by IFTTT if they have an image attached (because they came from Instagram)
if (!isset($tweet['retweet_status']) && strpos($tweet['source'], "IFTTT") && isset($tweet['entities']['media'])) continue;
// Skip any tweets created by Foursquare
if (!isset($tweet['retweet_status']) && strpos($tweet['source'], "Foursquare")) continue;
$extradata = array();
// Get the timestamp before we go down a layer to retweets
$timestamp = strtotime($tweet['created_at']);
// Set extra data if this is a retweet
if (isset($tweet['retweeted_status']))
{
$tweet = $tweet['retweeted_status'];
$extradata['opuser'] = $tweet['user']['screen_name'];
$extradata['opname'] = $tweet['user']['name'];
$extradata['opimg'] = $tweet['user']['profile_image_url_https'];
}
// Set extra data if there's an attached image
if (isset($tweet['entities']['media'])) $extradata['img'] = $tweet['entities']['media'][0]['media_url_https'];
// Convert any extra data into a JSON object
$extradata = (sizeof($extradata) > 0) ? json_encode($extradata) : "";
// Create a new FeedItem
$feeditem = new FeedItem;
$feeditem->timestamp = $timestamp;
$feeditem->channel_id = 2;
$feeditem->title = $tweet['full_text'];
$feeditem->link = "https://twitter.com/i/web/status/".$tweet['id_str'];
$feeditem->extra = $extradata;
$feeditem->save();
// Are there links to include?
if (sizeof($tweet['entities']['urls']) > 0)
{
foreach ($tweet['entities']['urls'] as $url)
{
$feeditem->links()->create([
'addr' => $url['url'],
'display' =>$url['display_url']
]);
}
}
// Are there tags to include?
if (sizeof($tweet['entities']['hashtags']))
{
foreach ($tweet['entities']['hashtags'] as $tag)
{
$feeditem->tags()->create([
'tag' => $tag['text']
]);
}
}
}
// Set the latest item ID
Settings::set('twitter_latest_tweet', $tweets[sizeof($tweets) - 1]['id_str']);
}
}
// Update the database with the latest instagram posts
public static function updateInstagram()
{
$posts = self::fetchInstagram();
if (sizeof($posts) > 0)
{
foreach ($posts as $post)
{
// Skip videos and albums
if ($post['media_type'] != "IMAGE") continue;
// Create a new FeedItem
$feeditem = new FeedItem;
$feeditem->timestamp = strtotime($post['timestamp']);
$feeditem->channel_id = 3;
$feeditem->title = (isset($post['caption'])) ? $post['caption'] : "";
$feeditem->link = $post['permalink'];
$feeditem->extra = json_encode(['img' => $post['media_url']]);
$feeditem->save();
// Are there tags to include?
if (isset($post['caption']) && strpos($post['caption'], '#'))
{
preg_match_all('%\B#\w*[a-zA-Z]+\w*%', $post['caption'], $hashtags);
foreach ($hashtags[0] as $tag)
{
$feeditem->tags()->create([
'tag' => substr($tag, 1)
]);
}
}
}
// Set the latest item ID
Settings::set('instagram_latest_post', strtotime(end($posts)['timestamp']));
}
}
// Update the database with the latest foursquare posts
public static function updateFoursquare()
{
$posts = self::fetchFoursquare();
if (sizeof($posts) > 0)
{
foreach ($posts as $post)
{
$extradata = [
'lat' => $post['venue']['location']['lat'],
'lng' => $post['venue']['location']['lng'],
'address' => []
];
// Build the address
if (isset($post['venue']['location']['address'])) $extradata['address'][] = $post['venue']['location']['address'];
if (isset($post['venue']['location']['city'])) $extradata['address'][] = $post['venue']['location']['city'];
if (isset($post['venue']['location']['country'])) $extradata['address'][] = $post['venue']['location']['country'];
$extradata['address'] = implode(', ', $extradata['address']);
// Create a new FeedItem
$feeditem = new FeedItem;
$feeditem->timestamp = $post['createdAt'];
$feeditem->channel_id = 6;
$feeditem->title = $post['venue']['name'];
$feeditem->body = (isset($post['shout'])) ? $post['shout'] : "";
$feeditem->extra = json_encode($extradata);
$feeditem->save();
// Are there tags to include?
if (isset($post['shout']) && strpos($post['shout'], '#'))
{
preg_match_all('%\B#\w*[a-zA-Z]+\w*%', $post['shout'], $hashtags);
foreach ($hashtags[0] as $tag)
{
$feeditem->tags()->create([
'tag' => substr($tag, 1)
]);
}
}
}
// Set the latest item ID
Settings::set('foursquare_latest_post', end($posts)['createdAt']);
}
}
// Fetch and return the latest tweets
private static function fetchTwitter()
{
// Set the API endpoint
$endpoint = "https://api.twitter.com/1.1/statuses/user_timeline.json";
// Set request parameteres
$req_params = array(
'screen_name' => Settings::get('twitter_username'),
'count' => 200,
'since_id' => Settings::get('twitter_latest_tweet'),
'tweet_mode' => 'extended',
'exclude_replies' => true,
'include_rts' => true
);
// Set oauth parameters
self::$authobject = array(
'oauth_token' => Settings::get('twitter_access_token'),
'oauth_consumer_key' => Settings::get('twitter_consumer_key'),
'oauth_nonce' => time(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0'
);
// Add parameters to oauth array
self::$authobject = array_merge(self::$authobject, $req_params);
// Generate the oauth signautre
$base_string = self::buildBaseString($endpoint, "GET", self::$authobject);
$composite_key = rawurlencode(Settings::get('twitter_consumer_secret'))."&".rawurlencode(Settings::get('twitter_access_token_secret'));
self::$authobject['oauth_signature'] = base64_encode(hash_hmac('sha1', $base_string, $composite_key, true));
// Create the URL by adding the parameters to the endpoint
$endpoint = $endpoint."?".http_build_query($req_params);
$tweets = array_reverse(json_decode(Http::get($endpoint, function($http)
{
$http->header(self::buildAuthorizationHeader(self::$authobject));
}), true));
return $tweets;
}
// Fetch and return the latest instagram posts
private static function fetchInstagram()
{
$endpoint = "https://graph.instagram.com/me/media";
// Set the fields we want back
$fields = [
'id',
'ig-id',
'caption',
'media_type',
'media_url',
'permalink',
'timestamp'
];
// Create the URL by adding the parameters to the endpoint
$endpoint = $endpoint."?".http_build_query([
'access_token' => Settings::get('instagram_access_token'),
'fields' => implode(',', $fields),
'limit' => 100
]);
$posts = json_decode(Http::get($endpoint), true);
$posts = array_reverse($posts['data']);
foreach($posts as $postkey => $post)
{
if (strtotime($post['timestamp']) <= Settings::get('instagram_latest_post')) unset($posts[$postkey]);
}
return $posts;
}
// Fetch and return the latest foursquare checkins
private static function fetchFoursquare()
{
$endpoint = "https://api.foursquare.com/v2/users/self/checkins";
// Create the URL by adding the parameters to the endpoint
$endpoint = $endpoint."?".http_build_query([
'oauth_token' => Settings::get('foursquare_access_token'),
'v' => date('Ymd'),
'limit' => 250,
'sort' => 'oldestfirst',
'afterTimestamp' => Settings::get('foursquare_latest_post') + 1
]);
$posts = json_decode(Http::get($endpoint), true);
return $posts['response']['checkins']['items'];
}
// Function to build a base string for the twitter API
private static function buildBaseString($baseURI, $method, $params) {
$r = array();
ksort($params);
foreach ($params as $key => $value) {
$r[] = $key.'='.rawurlencode($value);
}
return $method.'&'.rawurlencode($baseURI).'&'.rawurlencode(implode('&', $r));
}
// Function to build the OAuth authorization header for twitter
private static function buildAuthorizationHeader($oauth) {
$r = 'Authorization: OAuth ';
$values = array();
foreach($oauth as $key=>$value)
$values[] = $key.'="'.rawurlencode($value).'"';
$r .= implode(', ', $values);
return $r;
}
private static $authobject;
}

View File

@@ -0,0 +1,41 @@
<?php namespace JasonWilliams\Feed\Components;
use Cms\Classes\ComponentBase;
use JasonWilliams\Feed\Models\Channels;
class ChannelList extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Channel List',
'description' => 'Displays a list of feed channels.'
];
}
public function defineProperties()
{
return [
'includeEmpty' => [
'title' => 'Include Empty?',
'description' => 'Include channels with no posts?',
'type' => 'checkbox',
'default' => true
]
];
}
public function onRun()
{
$this->page['channels'] = Channels::orderBy('id')->withCount('feeditems')->get();
// If applicable, remove empty channels
if (!$this->property('includeEmpty'))
{
foreach($this->page['channels'] as $key => $value)
{
if (!$value->feeditems_count) unset($this->page['channels'][$key]);
}
}
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace JasonWilliams\Feed\Components;
use Cms\Classes\ComponentBase;
use JasonWilliams\Feed\Models\FeedItem;
class LazyLoadShortFeed extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Short Feed (Lazy Load)',
'description' => 'Displays a mini-feed of the most recent feed items, retrieved after other page content has been loaded'
];
}
public function defineProperties()
{
return [
'maxItems' => [
'title' => 'Max items',
'description' => 'How many feed items should be displayed?',
'default' => 10,
'type' => 'string',
'validationPattern' => '^[0-9]+$',
'validationMessage' => 'The Max items property must be numeric'
],
'renderPartial' => [
'title' => 'Render partial',
'description' => 'The path to the partial that will be used as a template to render feed items. @feed is provided by the plugin itself.',
'default' => '@feed',
'type' => 'string'
]
];
}
public function onRun()
{
$this->page['renderPartial'] = $this->property('renderPartial');
$this->addJs('/plugins/jasonwilliams/feed/assets/javascript/lazyloadshortfeed.js');
}
public function onUpdateRequested()
{
date_default_timezone_set('America/Edmonton');
$this->page['posts'] = FeedItem::orderBy('timestamp', 'desc')->take($this->property('maxItems'))->get();
}
}

View File

@@ -0,0 +1,79 @@
<?php namespace JasonWilliams\Feed\Components;
use Cms\Classes\ComponentBase;
use JasonWilliams\Feed\Models\FeedItem;
use JasonWilliams\Feed\Models\Tags;
class ShortFeed extends ComponentBase
{
private $channelfilter;
public function componentDetails()
{
return [
'name' => 'Short Feed',
'description' => 'Displays a mini-feed of the most recent feed items, loaded with other page content'
];
}
public function defineProperties()
{
return [
'channelFilter' => [
'title' => 'Channels',
'description' => 'A comma-separated list of channels to include in feed.',
'type' => 'string',
'default' => ''
],
'tagFilter' => [
'title' => 'Tag',
'description' => 'The tag to display in the feed.',
'type' => 'string',
'default' => ''
],
'maxItems' => [
'title' => 'Max items',
'description' => 'How many feed items should be displayed?',
'default' => 10,
'type' => 'string',
'validationPattern' => '^[0-9]+$',
'validationMessage' => 'The Max items property must be numeric'
]
];
}
public function onRun()
{
date_default_timezone_set('America/Edmonton');
// Set up the results query
$results = FeedItem::orderBy('timestamp', 'desc')->take($this->property('maxItems'));
// Do we need to filter based on a tag?
if ($this->property('tagFilter') != null)
{
$results->join('jasonwilliams_feed_tags', 'jasonwilliams_feed_.id', '=', 'jasonwilliams_feed_tags.feed_item_id');
$results->where('tag', $this->property('tagFilter'));
}
// Do we need to filter based on the channel?
if ($this->property('channelFilter') != null && $this->property('channelFilter') != 'all')
{
$this->channelfilter = explode(',', $this->property('channelFilter'));
$results->whereHas('channel', function($q) {
$firstchannel = true;
foreach ($this->channelfilter as $channel)
{
if ($firstchannel) $q->where('slug', $channel);
else $q->orWhere('slug', $channel);
$firstchannel = false;
}
});
}
$this->page['posts'] = $results->get();
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace JasonWilliams\Feed\Components;
use Db;
use Cms\Classes\ComponentBase;
use JasonWilliams\Feed\Models\Tags;
class TagList extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Tag List',
'description' => 'Displays a list of feed tags.'
];
}
public function defineProperties()
{
return [];
}
public function onRun()
{
$this->page['tags'] = Tags::groupBy('tag')->select(Db::raw('tag, count(*) as count'))->orderBy('count', 'desc')->get();
}
}

View File

@@ -0,0 +1,5 @@
<ul>
{% for channel in channels %}
<li><a href="/feed/{{ channel.slug }}">{{ channel.title }}</a></li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,4 @@
<div id="feedarea">Loading...</div>
{% put scripts %}
<script type="text/javascript">const renderPartial = '{{ renderPartial }}'</script>
{% endput %}

View File

@@ -0,0 +1,9 @@
<ul>
{% for post in posts %}
{% if post.title %}
<li>{{ post.title | raw }}</li>
{% else %}
<li>Post</li>
{% endif %}
{% endfor %}
</ul>

View File

@@ -0,0 +1,9 @@
<ul>
{% for post in posts %}
{% if post.title %}
<li>{{ post.title | raw }}</li>
{% else %}
<li>Post</li>
{% endif %}
{% endfor %}
</ul>

View File

@@ -0,0 +1,5 @@
<ul>
{% for tag in tags %}
<li><a href="{{ tag.tag }}">{{ tag.tag }}</a></li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,19 @@
<?php namespace jasonwilliams\feed\Controllers;
use Backend\Classes\Controller;
use BackendMenu;
class Channels extends Controller
{
public $implement = [ 'Backend\Behaviors\ListController', 'Backend\Behaviors\FormController', 'Backend\Behaviors\ReorderController' ];
public $listConfig = 'config_list.yaml';
public $formConfig = 'config_form.yaml';
public $reorderConfig = 'config_reorder.yaml';
public function __construct()
{
parent::__construct();
BackendMenu::setContext('jasonwilliams.feed', 'main-menu-item');
}
}

View File

@@ -0,0 +1,19 @@
<?php namespace jasonwilliams\feed\Controllers;
use Backend\Classes\Controller;
use BackendMenu;
class Feed extends Controller
{
public $implement = [ 'Backend\Behaviors\ListController', 'Backend\Behaviors\FormController', 'Backend\Behaviors\ReorderController' ];
public $listConfig = 'config_list.yaml';
public $formConfig = 'config_form.yaml';
public $reorderConfig = 'config_reorder.yaml';
public function __construct()
{
parent::__construct();
BackendMenu::setContext('jasonwilliams.feed', 'main-menu-item');
}
}

View File

@@ -0,0 +1,19 @@
<div data-control="toolbar">
<a href="<?= Backend::url('jasonwilliams/feed/channels/create') ?>" class="btn btn-primary oc-icon-plus"><?= e(trans('backend::lang.form.create')) ?></a>
<a href="<?= Backend::url('jasonwilliams/feed/channels/reorder') ?>" class="btn btn-default oc-icon-list"><?= e(trans('backend::lang.reorder.default_title')) ?></a>
<button
class="btn btn-default oc-icon-trash-o"
disabled="disabled"
onclick="$(this).data('request-data', {
checked: $('.control-list').listWidget('getChecked')
})"
data-request="onDelete"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', true)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>
</div>

View File

@@ -0,0 +1,3 @@
<div data-control="toolbar">
<a href="<?= Backend::url('jasonwilliams/feed/channels') ?>" class="btn btn-primary oc-icon-caret-left"><?= e(trans('backend::lang.form.return_to_list')) ?></a>
</div>

View File

@@ -0,0 +1,10 @@
name: Channels
form: $/jasonwilliams/feed/models/channels/fields.yaml
modelClass: jasonwilliams\feed\Models\Channels
defaultRedirect: jasonwilliams/feed/channels
create:
redirect: 'jasonwilliams/feed/channels/update/:id'
redirectClose: jasonwilliams/feed/channels
update:
redirect: jasonwilliams/feed/channels
redirectClose: jasonwilliams/feed/channels

View File

@@ -0,0 +1,12 @@
title: Channels
modelClass: jasonwilliams\feed\Models\Channels
list: $/jasonwilliams/feed/models/channels/columns.yaml
recordUrl: 'jasonwilliams/feed/channels/update/:id'
noRecordsMessage: 'backend::lang.list.no_records'
recordsPerPage: 20
showSetup: true
showCheckboxes: true
toolbar:
buttons: list_toolbar
search:
prompt: 'backend::lang.list.search_prompt'

View File

@@ -0,0 +1,4 @@
title: Channels
modelClass: jasonwilliams\feed\Models\Channels
toolbar:
buttons: reorder_toolbar

View File

@@ -0,0 +1,46 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/channels') ?>">Channels</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.create_and_close')) ?>
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('jasonwilliams/feed/channels') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('jasonwilliams/feed/channels') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
<?php endif ?>

View File

@@ -0,0 +1 @@
<?= $this->listRender() ?>

View File

@@ -0,0 +1,22 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/channels') ?>">Channels</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="form-preview">
<?= $this->formRenderPreview() ?>
</div>
<?php else: ?>
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<?php endif ?>
<p>
<a href="<?= Backend::url('jasonwilliams/feed/channels') ?>" class="btn btn-default oc-icon-chevron-left">
<?= e(trans('backend::lang.form.return_to_list')) ?>
</a>
</p>

View File

@@ -0,0 +1,8 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/channels') ?>">Channels</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?= $this->reorderRender() ?>

View File

@@ -0,0 +1,54 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/channels') ?>">Channels</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<button
type="button"
class="oc-icon-trash-o btn-icon danger pull-right"
data-request="onDelete"
data-load-indicator="<?= e(trans('backend::lang.form.deleting')) ?>"
data-request-confirm="<?= e(trans('backend::lang.form.confirm_delete')) ?>">
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('jasonwilliams/feed/channels') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('jasonwilliams/feed/channels') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
<?php endif ?>

View File

@@ -0,0 +1,19 @@
<div data-control="toolbar">
<a href="<?= Backend::url('jasonwilliams/feed/feed/create') ?>" class="btn btn-primary oc-icon-plus"><?= e(trans('backend::lang.form.create')) ?></a>
<a href="<?= Backend::url('jasonwilliams/feed/feed/reorder') ?>" class="btn btn-default oc-icon-list"><?= e(trans('backend::lang.reorder.default_title')) ?></a>
<button
class="btn btn-default oc-icon-trash-o"
disabled="disabled"
onclick="$(this).data('request-data', {
checked: $('.control-list').listWidget('getChecked')
})"
data-request="onDelete"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', true)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>
</div>

View File

@@ -0,0 +1,3 @@
<div data-control="toolbar">
<a href="<?= Backend::url('jasonwilliams/feed/feed') ?>" class="btn btn-primary oc-icon-caret-left"><?= e(trans('backend::lang.form.return_to_list')) ?></a>
</div>

View File

@@ -0,0 +1,10 @@
name: Feed
form: $/jasonwilliams/feed/models/feeditem/fields.yaml
modelClass: jasonwilliams\feed\Models\FeedItem
defaultRedirect: jasonwilliams/feed/feed
create:
redirect: 'jasonwilliams/feed/feed/update/:id'
redirectClose: jasonwilliams/feed/feed
update:
redirect: jasonwilliams/feed/feed
redirectClose: jasonwilliams/feed/feed

View File

@@ -0,0 +1,12 @@
title: Feed
modelClass: jasonwilliams\feed\Models\FeedItem
list: $/jasonwilliams/feed/models/feeditem/columns.yaml
recordUrl: 'jasonwilliams/feed/feed/update/:id'
noRecordsMessage: 'backend::lang.list.no_records'
recordsPerPage: 20
showSetup: true
showCheckboxes: true
toolbar:
buttons: list_toolbar
search:
prompt: 'backend::lang.list.search_prompt'

View File

@@ -0,0 +1,4 @@
title: Feed
modelClass: jasonwilliams\feed\Models\FeedItem
toolbar:
buttons: reorder_toolbar

View File

@@ -0,0 +1,46 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/feed') ?>">Feed</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.create_and_close')) ?>
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('jasonwilliams/feed/feed') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('jasonwilliams/feed/feed') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
<?php endif ?>

View File

@@ -0,0 +1 @@
<?= $this->listRender() ?>

View File

@@ -0,0 +1,22 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/feed') ?>">Feed</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="form-preview">
<?= $this->formRenderPreview() ?>
</div>
<?php else: ?>
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<?php endif ?>
<p>
<a href="<?= Backend::url('jasonwilliams/feed/feed') ?>" class="btn btn-default oc-icon-chevron-left">
<?= e(trans('backend::lang.form.return_to_list')) ?>
</a>
</p>

View File

@@ -0,0 +1,8 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/feed') ?>">Feed</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?= $this->reorderRender() ?>

View File

@@ -0,0 +1,54 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('jasonwilliams/feed/feed') ?>">Feed</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<button
type="button"
class="oc-icon-trash-o btn-icon danger pull-right"
data-request="onDelete"
data-load-indicator="<?= e(trans('backend::lang.form.deleting')) ?>"
data-request-confirm="<?= e(trans('backend::lang.form.confirm_delete')) ?>">
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('jasonwilliams/feed/feed') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('jasonwilliams/feed/feed') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
<?php endif ?>

View File

@@ -0,0 +1,6 @@
<?php return [
'plugin' => [
'name' => 'Social Feed',
'description' => 'Captures social media posts from a variety of services and consolidates them into a single feed.'
]
];

View File

@@ -0,0 +1,33 @@
<?php namespace jasonwilliams\feed\Models;
use Model;
/**
* Model
*/
class Channels extends Model
{
use \October\Rain\Database\Traits\Validation;
/*
* Disable timestamps by default.
* Remove this line if timestamps are defined in the database table.
*/
public $timestamps = false;
/**
* @var string The database table used by the model.
*/
public $table = 'jasonwilliams_feed_channels';
/**
* @var array Validation rules
*/
public $rules = [
];
public $hasMany = [
'feeditems' => ['jasonwilliams\feed\Models\FeedItem', 'key' => 'channel_id']
];
}

View File

@@ -0,0 +1,65 @@
<?php namespace jasonwilliams\feed\Models;
use Model;
/**
* Model
*/
class FeedItem extends Model
{
use \October\Rain\Database\Traits\Validation;
/*
* Disable timestamps by default.
* Remove this line if timestamps are defined in the database table.
*/
public $timestamps = false;
/**
* @var string The database table used by the model.
*/
public $table = 'jasonwilliams_feed_';
/**
* @var array Validation rules
*/
public $rules = [
];
public $hasMany = [
'links' => 'jasonwilliams\feed\Models\Links',
'tags' => 'jasonwilliams\feed\Models\Tags'
];
public $belongsTo = [
'channel' => ['jasonwilliams\feed\Models\Channels', 'key' => 'channel_id']
];
public function afterFetch()
{
// Add clickable hashtags to the title field
$this->title = preg_replace('/#(\w*[a-zA-Z]+\w*)/', '<a href="/feed/all/$1">#$1</a>', $this->title);
// Add a friendlytime field
$this->friendlytime = date('M j, Y', $this->timestamp);
// Expand extras to an object
if ($this->extra) $this->extra = json_decode($this->extra);
// Add twitter user links
if ($this->channel_id == 2) $this->title = preg_replace('/@([a-zA-Z0-9_]+)/', '<a href="http://twitter.com/$1">@$1</a>', $this->title);
// Add instagram user links
if ($this->channel_id == 3) $this->title = preg_replace('/@([a-zA-Z0-9_]+)/', '<a href="https://www.instagram.com/$1">@$1</a>', $this->title);
// Add foursquare user Links
if ($this->channel_id == 6 && !empty($this->body)) $this->body = preg_replace('/@([a-zA-Z0-9_]+)/', '<a href="https://www.foursquare.com/$1">@$1</a>', $this->body);
// Find and replace links
foreach($this->links as $link)
{
$this->title = str_replace($link->addr, "<a href=\"".$link->addr."\">".$link->display."</a>", $this->title);
}
}
}

View File

@@ -0,0 +1,35 @@
<?php namespace jasonwilliams\feed\Models;
use Model;
/**
* Model
*/
class Links extends Model
{
use \October\Rain\Database\Traits\Validation;
/*
* Disable timestamps by default.
* Remove this line if timestamps are defined in the database table.
*/
public $timestamps = false;
/**
* @var string The database table used by the model.
*/
public $table = 'jasonwilliams_feed_links';
protected $fillable = ['addr', 'display'];
/**
* @var array Validation rules
*/
public $rules = [
];
public $belongsTo = [
'links' => 'jasonwilliams\feed\Models\FeedItem'
];
}

View File

@@ -0,0 +1,14 @@
<?php namespace JasonWilliams\Feed\Models;
use Model;
class Settings extends Model
{
public $implement = ['System.Behaviors.SettingsModel'];
// A unique code
public $settingsCode = 'jasonwilliams_feed_settings';
// Reference to field configuration
public $settingsFields = 'fields.yaml';
}

View File

@@ -0,0 +1,40 @@
<?php namespace jasonwilliams\feed\Models;
use Model;
/**
* Model
*/
class Tags extends Model
{
use \October\Rain\Database\Traits\Validation;
/*
* Disable timestamps by default.
* Remove this line if timestamps are defined in the database table.
*/
public $timestamps = false;
/**
* @var string The database table used by the model.
*/
public $table = 'jasonwilliams_feed_tags';
protected $fillable = ['tag'];
/**
* @var array Validation rules
*/
public $rules = [
];
public $belongsTo = [
'feeditem' => ['jasonwilliams\feed\Models\FeedItem', 'key' => 'feed_item_id']
];
public function afterFetch()
{
//dd($this->feeditem);
}
}

View File

@@ -0,0 +1,10 @@
columns:
id:
label: 'Channel ID'
type: number
sortable: true
title:
label: Title
type: text
searchable: true
sortable: false

View File

@@ -0,0 +1,21 @@
fields:
title:
label: 'Channel Title'
span: left
type: text
slug:
label: Slug
span: right
preset:
field: title
type: slug
type: text
icon:
label: Icon
span: left
type: text
comment: 'Icon code from FontAwesome'
colour:
label: 'Icon Colour'
span: auto
type: colorpicker

View File

@@ -0,0 +1,9 @@
columns:
title:
label: 'Post Title'
type: text
searchable: true
timestamp:
label: 'UNIX Timestamp'
type: number
sortable: true

View File

@@ -0,0 +1,20 @@
fields:
title:
label: 'Post Title'
span: full
type: text
body:
label: 'Post Body'
size: ''
span: full
type: richeditor
timestamp:
label: 'UNIX Timestamp'
span: auto
type: number
channel:
label: Channel
nameFrom: title
descriptionFrom: description
span: auto
type: relation

View File

@@ -0,0 +1,51 @@
fields:
twitter_section:
label: Twitter API Settings
type: section
twitter_username:
label: Username
description: Your twitter username, excluding the @
span: left
twitter_latest_tweet:
label: Latest Tweet ID
description: The ID of the most recent tweet already fetched and stored
span: right
twitter_consumer_key:
label: Consumer Key
description: Your consumer key for accessing the twitter API
span: left
twitter_consumer_secret:
label: Consumer Key Secret
description: Your consumer key secret for accessing the twitter API
span: right
twitter_access_token:
label: Access Token
description: Your access token for accessing the twitter API
span: left
twitter_access_token_secret:
label: Access Token Secret
description: Your access token secret for accessing the twitter API
span: right
instagram_section:
label: Instagram API Settings
type: section
instagram_access_token:
label: Access Token
description: Your access token for accessing the instagram API
span: full
instagram_latest_post:
label: Latest Post Timestamp
description: The unix timestamp of the most recent post already fetched and stored
span: left
foursquare_section:
label: Foursquare API Settings
type: section
foursquare_access_token:
description: Your access token for accessing the foursquare API
span: full
foursquare_latest_post:
label: Latest Checkin Timestamp
description: The unix timestamp of the most recent checkin already fetched and stored
span: left

View File

@@ -0,0 +1,20 @@
plugin:
name: 'jasonwilliams.feed::lang.plugin.name'
description: 'jasonwilliams.feed::lang.plugin.description'
author: 'Jason Williams'
icon: oc-icon-newspaper-o
homepage: 'https://ja.son-williams.ca/'
navigation:
main-menu-item:
label: Feed
url: jasonwilliams/feed/feed
icon: icon-newspaper-o
sideMenu:
side-menu-item:
label: 'Social Feed'
url: jasonwilliams/feed/feed
icon: icon-sitemap
side-menu-item2:
label: 'Social Channels'
url: jasonwilliams/feed/channels
icon: icon-sitemap

View File

@@ -0,0 +1,57 @@
<?php namespace jasonwilliams\feed\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class JasonwilliamsFeedInitialSetup extends Migration
{
public function up()
{
Schema::create('jasonwilliams_feed_', function($table)
{
$table->engine = 'InnoDB';
$table->increments('id')->unsigned();
$table->integer('timestamp')->unsigned();
$table->integer('channel_id')->unsigned();
$table->string('title', 512)->nullable();
$table->string('link', 256)->nullable();
$table->text('body')->nullable();
$table->text('extra');
});
Schema::create('jasonwilliams_feed_tags', function($table)
{
$table->engine = 'InnoDB';
$table->increments('id')->unsigned();
$table->string('tag', 128);
$table->integer('feed_item_id')->unsigned();
});
Schema::create('jasonwilliams_feed_links', function($table)
{
$table->engine = 'InnoDB';
$table->increments('id')->unsigned();
$table->string('addr', 256);
$table->string('display', 256);
$table->integer('feed_item_id')->unsigned();
});
Schema::create('jasonwilliams_feed_channels', function($table)
{
$table->engine = 'InnoDB';
$table->increments('id')->unsigned();
$table->string('title', 64);
$table->string('slug', 64);
$table->string('icon', 128)->nullable();
$table->string('colour', 7)->nullable();
});
}
public function down()
{
Schema::dropIfExists('jasonwilliams_feed_');
Schema::dropIfExists('jasonwilliams_feed_tags');
Schema::dropIfExists('jasonwilliams_feed_links');
Schema::dropIfExists('jasonwilliams_feed_channels');
}
}

View File

@@ -0,0 +1,3 @@
1.0.1:
- 'Initialize plugin and create database tables.'
- jasonwilliams_feed_initial_setup.php