diff --git a/.gitignore b/.gitignore index 07fe371..207052b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ vendor settings.php nbproject/private -*.sync-conflict* \ No newline at end of file +*.sync-conflict* +/database.mwb.bak \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 7cba5c4..ea9ed74 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "static/css/material-color"] path = static/css/material-color url = https://source.netsyms.com/Netsyms/Material-Color +[submodule "static/Shuffle"] + path = static/Shuffle + url = https://source.netsyms.com/Mirrors/Vestride_Shuffle.git diff --git a/action.php b/action.php index 67b230b..28f6029 100644 --- a/action.php +++ b/action.php @@ -35,4 +35,8 @@ switch ($VARS['action']) { session_destroy(); header('Location: index.php?logout=1'); die("Logged out."); + case "thumbnail": + header("Content-Type: image/jpeg"); + // TODO + break; } \ No newline at end of file diff --git a/composer.json b/composer.json index fb37c32..9655a01 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "netsyms/business-app-template", - "description": "Template for a webapp integrated with an AccountHub server.", + "name": "netsyms/todaystream", + "description": "News and weather app.", "type": "project", "require": { "catfan/medoo": "^1.5", diff --git a/composer.lock b/composer.lock index 8d36028..5f8a417 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "5c7439c6e041764f2f6b0270a95ab3ae", - "content-hash": "e4e700119f47d2f68b0ed82abaf8c5c6", + "content-hash": "5642e5d15688c9510f4b0d1b4ceed67e", "packages": [ { "name": "catfan/medoo", - "version": "v1.5.7", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/catfan/Medoo.git", - "reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf" + "reference": "53a02b300d673f716cb06bf0e24fd774ec53939f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/catfan/Medoo/zipball/8d90cba0e8ff176028847527d0ea76fe41a06ecf", - "reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf", + "url": "https://api.github.com/repos/catfan/Medoo/zipball/53a02b300d673f716cb06bf0e24fd774ec53939f", + "reference": "53a02b300d673f716cb06bf0e24fd774ec53939f", "shasum": "" }, "require": { @@ -64,7 +63,7 @@ "sql", "sqlite" ], - "time": "2018-06-14 18:59:08" + "time": "2018-12-08T20:24:23+00:00" }, { "name": "guzzlehttp/guzzle", @@ -129,7 +128,7 @@ "rest", "web service" ], - "time": "2018-04-22 15:46:56" + "time": "2018-04-22T15:46:56+00:00" }, { "name": "guzzlehttp/promises", @@ -180,36 +179,37 @@ "keywords": [ "promise" ], - "time": "2016-12-20 10:07:11" + "time": "2016-12-20T10:07:11+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.4.2", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + "reference": "9f83dded91781a01c63574e387eaa769be769115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -239,13 +239,14 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-03-20 17:10:46" + "time": "2018-12-04T20:46:45+00:00" }, { "name": "psr/http-message", @@ -295,7 +296,47 @@ "request", "response" ], - "time": "2016-08-06 14:39:51" + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" } ], "packages-dev": [], diff --git a/database.mwb b/database.mwb new file mode 100644 index 0000000..26402ce Binary files /dev/null and b/database.mwb differ diff --git a/langs/en/categories.json b/langs/en/categories.json new file mode 100644 index 0000000..5395e91 --- /dev/null +++ b/langs/en/categories.json @@ -0,0 +1,9 @@ +{ + "business": "Business", + "entertainment": "Entertainment", + "general": "General", + "health": "Health", + "science": "Science", + "sports": "Sports", + "technology": "Technology" +} diff --git a/lib/ApiFetcher.lib.php b/lib/ApiFetcher.lib.php new file mode 100644 index 0000000..37f9189 --- /dev/null +++ b/lib/ApiFetcher.lib.php @@ -0,0 +1,85 @@ +value array of URL parameters + * @param array $extraParams key=>value array of URL parameters that will be ignored when checking the cache + * @param string $expires When the fetched resource should expire from the cache. A string parsable by strtotime() + * @return string The content of the requested URL. + */ + static function get(string $url, array $params = [], array $extraParams = [], string $expires = "+15 minutes"): string { + global $database; + + // Delete stale records + $database->delete("requestcache", ["expires[<=]" => date("Y-m-d H:i:s")]); + + // Make sure the params are in the same order every time + ksort($params); + + $urlparams = []; + foreach ($params as $k => $v) { + $urlparams[] = urlencode($k) . "=" . urlencode($v); + } + + // Make the URL that will be cached + $cacheurl = $url . "?" . implode("&", $urlparams); + + foreach ($extraParams as $k => $v) { + $urlparams[] = urlencode($k) . "=" . urlencode($v); + } + + if ($database->has("requestcache", ["AND" => ["url" => $cacheurl, "expires[>]" => date("Y-m-d H:i:s")]])) { + return $database->get("requestcache", "content", ["AND" => ["url" => $cacheurl, "expires[>]" => date("Y-m-d H:i:s")]]); + } + + // Make the actual URL that will be requested + $requesturl = $url . "?" . implode("&", $urlparams); + + $content = file_get_contents($requesturl); + + // Only insert into db if it didn't fail horribly + if ($content !== FALSE) { + $database->insert("requestcache", ["url" => $cacheurl, "expires" => date("Y-m-d H:i:s", strtotime($expires)), "content" => $content]); + } + + return $content; + } + + /** + * Clear the given URL from the cache. + * + * @global Medoo $database + * @param string $url + * @param array $params key=>value array of URL parameters + */ + static function removeFromCache(string $url = "", array $params = []) { + global $database; + + // Make sure the params are in the same order every time + ksort($params); + + $urlparams = []; + foreach ($params as $k => $v) { + $urlparams[] = urlencode($k) . "=" . urlencode($v); + } + + $cacheurl = $url . "?" . implode("&", $urlparams); + + $database->delete("requestcache", ["url" => $cacheurl]); + } + +} diff --git a/lib/News.lib.php b/lib/News.lib.php new file mode 100644 index 0000000..5a1bca7 --- /dev/null +++ b/lib/News.lib.php @@ -0,0 +1,37 @@ +loadItems(); + News::$items = array_merge(News::$items, $source->getItems()); + } + } + + /** + * Get all news items from all sources loaded. + * @return array + */ + static function getItems() { + return News::$items; + } + +} \ No newline at end of file diff --git a/lib/NewsCategory.lib.php b/lib/NewsCategory.lib.php new file mode 100644 index 0000000..0deb6ef --- /dev/null +++ b/lib/NewsCategory.lib.php @@ -0,0 +1,123 @@ + "Business", + self::ENTERTAINMENT => "Entertainment", + self::GENERAL => "General", + self::HEALTH => "Health", + self::SCIENCE => "Science", + self::SPORTS => "Sports", + self::TECHNOLOGY => "Technology" + ]; + + public function __construct(int $category) { + $this->category = $category; + } + + /** + * Get the category as an int corresponding to one of the constants. + * @return int + */ + public function get(): int { + return $this->category; + } + + /** + * Get a string representation of the category. + * @return string + */ + public function toString(): string { + switch ($this->category) { + case self::BUSINESS: + return "business"; + case self::ENTERTAINMENT: + return "entertainment"; + case self::GENERAL: + return "general"; + case self::HEALTH: + return "health"; + case self::SCIENCE: + return "science"; + case self::SPORTS: + return "sports"; + case self::TECHNOLOGY: + return "technology"; + default: + return ""; + } + } + + public static function fromString(string $category): NewsCategory { + $cat = self::NONE_ALL; + switch (strtolower($category)) { + case "business": + $cat = self::BUSINESS; + break; + case "entertainment": + $cat = self::ENTERTAINMENT; + break; + case "general": + $cat = self::GENERAL; + break; + case "health": + $cat = self::HEALTH; + break; + case "science": + $cat = self::SCIENCE; + break; + case "sports": + $cat = self::SPORTS; + break; + case "technology": + case "tech": + $cat = self::TECHNOLOGY; + break; + } + return new NewsCategory($cat); + } + + /** + * Get a suitable FontAwesome 5 icon for the category. + * @return string CSS classes, such as "fas fa-icon". + */ + public function getIcon(): string { + switch ($this->category) { + case self::BUSINESS: + return "fas fa-briefcase"; + case self::ENTERTAINMENT: + return "fas fa-tv"; + case self::GENERAL: + return "fas fa-info-circle"; + case self::HEALTH: + return "fas fa-heartbeat"; + case self::SCIENCE: + return "fas fa-atom"; + case self::SPORTS: + return "fas fa-futbol"; + case self::TECHNOLOGY: + return "fas fa-laptop"; + default: + return "fas fa-newspaper"; + } + } + +} diff --git a/lib/NewsItem.lib.php b/lib/NewsItem.lib.php new file mode 100644 index 0000000..ba39699 --- /dev/null +++ b/lib/NewsItem.lib.php @@ -0,0 +1,61 @@ +headline = $headline; + $this->img = $img; + $this->url = $url; + $this->source = $source; + $this->via = $via; + $this->timestamp = $timestamp; + if (is_null($category)) { + $this->category = new NewsCategory(NewsCategory::GENERAL); + } else { + $this->category = $category; + } + } + + function getImage(): string { + return $this->img; + } + + function getURL(): string { + return $this->url; + } + + function getHeadline(): string { + return $this->headline; + } + + function getSource(): string { + return $this->source; + } + + function getVia(): string { + return $this->via; + } + + function getTimestamp(): int { + return $this->timestamp; + } + + function getCategory(): \NewsCategory { + return $this->category; + } + +} diff --git a/lib/NewsSource.lib.php b/lib/NewsSource.lib.php new file mode 100644 index 0000000..4883fd8 --- /dev/null +++ b/lib/NewsSource.lib.php @@ -0,0 +1,27 @@ +items; + } + + /** + * Fetch news items from this source. + */ + function loadItems() { + // Do nothing, because this is a dummy source + } + +} \ No newline at end of file diff --git a/lib/NewsSource_NewsAPI.lib.php b/lib/NewsSource_NewsAPI.lib.php new file mode 100644 index 0000000..b8ae4d0 --- /dev/null +++ b/lib/NewsSource_NewsAPI.lib.php @@ -0,0 +1,66 @@ +items; + } + + function loadItems() { + $this->loadHeadlines((new NewsCategory(NewsCategory::BUSINESS))); + $this->loadHeadlines((new NewsCategory(NewsCategory::ENTERTAINMENT))); + $this->loadHeadlines((new NewsCategory(NewsCategory::GENERAL))); + $this->loadHeadlines((new NewsCategory(NewsCategory::HEALTH))); + $this->loadHeadlines((new NewsCategory(NewsCategory::SCIENCE))); + $this->loadHeadlines((new NewsCategory(NewsCategory::SPORTS))); + $this->loadHeadlines((new NewsCategory(NewsCategory::TECHNOLOGY))); + } + + private function loadHeadlines($category = null, string $country = "us", $apikey = null) { + global $SETTINGS; + if (is_null($category)) { + $category = new NewsCategory(NewsCategory::GENERAL); + } + if (is_null($apikey)) { + $apikey = $SETTINGS['apikeys']['newsapi.org']; + } + $url = "https://newsapi.org/v2/top-headlines"; + $params = []; + if (!empty($country)) { + $params['country'] = $country; + } + if (!empty($category->toString())) { + $params['category'] = $category->toString(); + } + $items = []; + $json = ApiFetcher::get($url, $params, ["apiKey" => $apikey]); + $data = json_decode($json, TRUE); + if ($data['status'] != "ok") { + return []; + } + $articles = $data['articles']; + foreach ($articles as $n) { + $title = $n['title']; + $image = $n['urlToImage']; + if (is_null($image)) { + continue; + } + $url = $n['url']; + $source = $n['source']['name']; + $timestamp = strtotime($n['publishedAt']); + + $items[] = new NewsItem($title, $image, $url, $source, "NewsAPI.org", $timestamp, $category); + } + + $this->items = array_merge($this->items, $items); + } + +} diff --git a/lib/NewsSource_Reddit.lib.php b/lib/NewsSource_Reddit.lib.php new file mode 100644 index 0000000..e6d4a59 --- /dev/null +++ b/lib/NewsSource_Reddit.lib.php @@ -0,0 +1,72 @@ +items; + } + + function loadItems() { + $this->loadSubreddit("news+worldnews+UpliftingNews", new NewsCategory(NewsCategory::GENERAL)); + //$this->loadSubreddit("technology", new NewsCategory(NewsCategory::TECHNOLOGY)); + } + + private function loadSubreddit(string $subreddit, NewsCategory $category) { + $items = []; + $json = ApiFetcher::get("https://www.reddit.com/r/$subreddit.json", ["limit" => "50"]); + $news = json_decode($json, TRUE)['data']['children']; + foreach ($news as $d) { + $n = $d['data']; + + // Ignore non-linky or NSFW posts + if ($n['is_self']) { + continue; + } + if ($n['is_video']) { + continue; + } + if ($n['over_18']) { + continue; + } + if (empty($n['url'])) { + continue; + } + + // Thumbnail image + $image = ""; + if (isset($n['thumbnail']) && $n['thumbnail'] != "default") { + $image = $n['thumbnail']; + } + if (isset($n['preview']['images'][0]['resolutions'])) { + if (isset($n['preview']['images'][0]['resolutions'][2]['url'])) { + $image = $n['preview']['images'][0]['resolutions'][2]['url']; + } else if (isset($n['preview']['images'][0]['resolutions'][1]['url'])) { + $image = $n['preview']['images'][0]['resolutions'][1]['url']; + } + } + + $source = "reddit.com"; + if (!empty($n['domain'])) { + $source = $n['domain']; + } + + $timestamp = time(); + if (!empty($n['created'])) { + $timestamp = $n['created']; + } + + $items[] = new NewsItem($n['title'], $image, $n['url'], $source, "reddit", $timestamp, $category); + } + + $this->items = array_merge($this->items, $items); + } + +} diff --git a/lib/Thumbnail.lib.php b/lib/Thumbnail.lib.php new file mode 100644 index 0000000..2f0addf --- /dev/null +++ b/lib/Thumbnail.lib.php @@ -0,0 +1,60 @@ +org.netbeans.modules.php.project - BusinessAppTemplate + TodayStream diff --git a/pages.php b/pages.php index fe7cc1c..23cf0d4 100644 --- a/pages.php +++ b/pages.php @@ -7,19 +7,24 @@ // List of pages and metadata define("PAGES", [ "home" => [ - "title" => "Home", + "title" => "Overview", "navbar" => true, - "icon" => "fas fa-home" - ], - "404" => [ - "title" => "404 error" + "icon" => "fas fa-home", + "scripts" => [ + "static/Shuffle/dist/shuffle.min.js", + "static/js/home.js" + ] ], - "form" => [ - "title" => "Form", + "news" => [ + "title" => "News", "navbar" => true, - "icon" => "fas fa-file-alt", + "icon" => "fas fa-newspaper", "scripts" => [ - "static/js/form.js" + "static/Shuffle/dist/shuffle.min.js", + "static/js/news.js" ] + ], + "404" => [ + "title" => "404 error" ] ]); \ No newline at end of file diff --git a/pages/home.php b/pages/home.php index 1d44fbf..d101aa9 100644 --- a/pages/home.php +++ b/pages/home.php @@ -1,6 +1,54 @@ getCategory()->toString()][] = $item; +} ?> -

Hello World

\ No newline at end of file + + $items) { + $cat = NewsCategory::fromString($category); + ?> +

+ get($cat->toString()); ?> +

+
+ = 5) { + break; + } + $count++; + ?> +
+ +

+ getHeadline()); ?> +

+

getSource()); ?>

+
+ + getImage())) { ?> + + +
+ +
+ \ No newline at end of file diff --git a/pages/news.php b/pages/news.php new file mode 100644 index 0000000..7a71fa5 --- /dev/null +++ b/pages/news.php @@ -0,0 +1,41 @@ + + + + +
+ + +
+ +
+ + +
+ +
diff --git a/settings.template.php b/settings.template.php index 94686c0..36543b0 100644 --- a/settings.template.php +++ b/settings.template.php @@ -19,14 +19,14 @@ $SETTINGS = [ // See http://medoo.in/api/new for info "database" => [ "type" => "mysql", - "name" => "app", + "name" => "todaystream", "server" => "localhost", - "user" => "app", + "user" => "", "password" => "", "charset" => "utf8" ], // Name of the app. - "site_title" => "Web App Template", + "site_title" => "TodayStream", // Settings for connecting to the AccountHub server. "accounthub" => [ // URL for the API endpoint @@ -36,6 +36,21 @@ $SETTINGS = [ // API key "key" => "123" ], + // API keys for data sources + "apikeys" => [ + "newsapi.org" => "", + "darksky.net" => "", + ], + // Which data sources to use + "sources" => [ + "news" => [ + "NewsAPI", + "Reddit" + ], + "weather" => [ + "DarkSky" + ] + ], // List of required user permissions to access this app. "permissions" => [ ], @@ -44,7 +59,7 @@ $SETTINGS = [ // Language to use for localization. See langs folder to add a language. "language" => "en", // Shown in the footer of all the pages. - "footer_text" => "", + "footer_text" => "Data sources: NewsAPI.org, Reddit, Dark Sky", // Also shown in the footer, but with "Copyright " in front. "copyright" => "Netsyms Technologies", // Base URL for building links relative to the location of the app. diff --git a/static/Shuffle b/static/Shuffle new file mode 160000 index 0000000..c742bfa --- /dev/null +++ b/static/Shuffle @@ -0,0 +1 @@ +Subproject commit c742bfaefc8908170825227a7385f20e0ba5da37 diff --git a/static/js/news.js b/static/js/news.js new file mode 100644 index 0000000..f51f399 --- /dev/null +++ b/static/js/news.js @@ -0,0 +1,19 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +window.shuffleInstance = new window.Shuffle(document.getElementById('news-grid'), { + itemSelector: '.grid__brick', + sizer: '.sizer-element' +}); + +$("input[name=newscategory]").on("change", function () { + window.shuffleInstance.filter($(this).val()); + $(this).button('toggle'); +}); + +setInterval(function () { + window.shuffleInstance.layout(); +}, 500); \ No newline at end of file