aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <hello@cleberg.net>2023-06-14 22:08:34 -0500
committerChristian Cleberg <hello@cleberg.net>2023-06-14 22:08:34 -0500
commit49efb238b879bce764d04dad99c7a169e80f93dd (patch)
tree7b1d26dbe86710ee7830b3fe6ff82afff3712e2d
parent53488151f29e3afbd1b1348597ff2123b97ad2d7 (diff)
downloadhn-49efb238b879bce764d04dad99c7a169e80f93dd.tar.gz
hn-49efb238b879bce764d04dad99c7a169e80f93dd.tar.bz2
hn-49efb238b879bce764d04dad99c7a169e80f93dd.zip
massive overhaul to implement proper MVC
-rw-r--r--CODE_OF_CONDUCT.md4
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--README.md34
-rw-r--r--index.php238
-rw-r--r--src/Controller/FeedController.php47
-rw-r--r--src/Controller/RouteController.php171
-rw-r--r--src/Model/ApiService.php218
-rw-r--r--src/Model/CacheService.php10
-rw-r--r--src/View/BaseTemplate.php35
-rw-r--r--src/View/class-template.php58
-rw-r--r--static/styles.css187
-rw-r--r--static/styles.min.css2
-rw-r--r--templates/template.html36
13 files changed, 674 insertions, 370 deletions
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 58fda5d..e16a1bf 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -2,7 +2,7 @@
## Summary
-Be logical and act in a way that represents your values. The following values
+Be logical and act in a way that represents your values. The following values
are inherent for this project:
- Respect
@@ -11,5 +11,5 @@ are inherent for this project:
## Reporting
-If you find anything that goes against this code of conduct or is offensive,
+If you find anything that goes against this code of conduct or is offensive,
please report it to the maintainers.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6367dc1..d03b13c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,6 +10,6 @@ This project really only has a few guidelines to follow:
## Reporting Bugs
-I don't use a report template, so just remember to give as much detail as
-possible about the bug you're experiencing. If there's not enough detail
+I don't use a report template, so just remember to give as much detail as
+possible about the bug you're experiencing. If there's not enough detail
reported, emails will need to be exchanged before anyone can look into the bug.
diff --git a/README.md b/README.md
index 8aff18b..4b4bf70 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,18 @@
# hn
-[hn](https://hn.cleberg.net) is a simple front-end alternative for Hacker
+[hn](https://hn.cleberg.net) is a simple front-end alternative for Hacker
News, focusing on privacy and simplicity.
## Getting Started
-These instructions will get you a copy of the project up and running on your
-local machine for development and testing purposes. See deployment for notes on
+These instructions will get you a copy of the project up and running on your
+local machine for development and testing purposes. See deployment for notes on
how to deploy the project on a live system.
### Prerequisites
- A web server (e.g., Nginx or Apache)
-- PHP
+- PHP (>= v8.0)
- Optional: minify
### Installing
@@ -20,7 +20,7 @@ how to deploy the project on a live system.
Install the dependencies, using the web server of your choice:
```
-sudo apt install nginx-full php minify
+sudo apt install nginx-full php php-cgi php-fpm minify
```
Clone the repo:
@@ -38,11 +38,11 @@ minify -o static/styles.min.css static/styles.css
## Deployment
-Deployment is as easy as copying the code to your webroot. No special packages
+Deployment is as easy as copying the code to your webroot. No special packages
or tools required.
-To deploy, ensure you have a publicly-available web server and configure it to
-fallback with all errors to the `index.php` file rather than returning a `404`
+To deploy, ensure you have a publicly-available web server and configure it to
+fallback with all errors to the `index.php` file rather than returning a `404`
error.
For nginx, include the following snippet in your website's conf file:
@@ -55,7 +55,7 @@ location / {
}
```
-For Apache, you can include the following snippet in a `.htaccess` file within
+For Apache, you can include the following snippet in a `.htaccess` file within
the directory you're serving the PHP file from:
```conf
@@ -66,12 +66,13 @@ FallbackResource /index.php
* [PHP](https://www.php.net/) - The scripting language
* [HTML](https://html.spec.whatwg.org/multipage/) - The markup language
-* [minify](https://github.com/tdewolff/minify/tree/master/cmd/minify) - Used to
-minify CSS
+* [minify](https://github.com/tdewolff/minify/tree/master/cmd/minify) - Used to
+ minify CSS
## Contributing
-Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of
+Please read [CONTRIBUTING.md](./CONTRIBUTING.md) and
+[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) for details on our code of
conduct, and the process for submitting pull requests to us.
### TODO
@@ -79,9 +80,10 @@ conduct, and the process for submitting pull requests to us.
A scratch pad of ideas that may be useful to implement:
- [x] Add minimal CSS.
-- [~] Add functionality to view a user's profile.
-- [ ] Add functionality to view item-specific page with comments.
-- [ ] Add functionality to load more items or paginate?
+- [x] Add functionality to view a user's profile.
+- [ ] Add functionality to view item-specific page with comments (`ConstructStoryDiscussion`).
+- [ ] Add functionality to handle polls (`ConstructPoll` & `ConstructPollOpt`).
+- [ ] Add functionality to load more items or paginate.
## Versioning
@@ -93,5 +95,5 @@ This project currently doesn't use versioning. See the git log instead.
## License
-This project is licensed under the Unlicense - see the
+This project is licensed under the Unlicense - see the
[LICENSE.md](./LICENSE.md) file for details.
diff --git a/index.php b/index.php
index f85b25b..03ecb6a 100644
--- a/index.php
+++ b/index.php
@@ -1,236 +1,10 @@
<?php
-$full_domain = 'https://hn.cleberg.net';
-$path = ltrim($_SERVER['REQUEST_URI'], '/');
-$elements = explode('/', $path);
+require_once 'src/Controller/RouteController.php';
-if (empty($elements[0])) {
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/topstories.json?limitToFirst=10&orderBy="$key"',
- 'Top'
- );
- echo_html(
- $GLOBALS['full_domain'],
- 'The top stories from Hacker News, proxied by hn.',
- 'hn',
- $html_output
- );
-} else {
- switch (array_shift($elements)) {
- case 'top':
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/topstories.json?limitToFirst=10&orderBy="$key"',
- 'Top'
- );
- echo_html(
- $GLOBALS['full_domain'],
- 'The top stories from Hacker News, proxied by hn.',
- 'hn',
- $html_output
- );
- break;
+$GLOBALS['full_domain'] = 'https://hn.cleberg.net';
+$GLOBALS['author_name'] = 'cmc';
+$GLOBALS['site_title'] = 'hn';
- case 'best':
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/beststories.json?limitToFirst=10&orderBy="$key"',
- 'Best'
- );
- echo_html(
- $GLOBALS['full_domain'] . '/best/',
- 'The best 30 stories from Hacker News, proxied by hn.',
- 'hn ~ best',
- $html_output
- );
- break;
-
- case 'new':
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/newstories.json?limitToFirst=10&orderBy="$key"',
- 'New'
- );
- echo_html(
- $GLOBALS['full_domain'] . '/new/',
- 'The newest 30 stories from Hacker News, proxied by hn.',
- 'hn ~ new',
- $html_output
- );
- break;
-
- case 'ask':
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/askstories.json?limitToFirst=10&orderBy="$key"',
- 'Ask'
- );
- echo_html(
- $GLOBALS['full_domain'] . '/ask/',
- 'The latest 30 asks from Hacker News, proxied by hn.',
- 'hn ~ ask',
- $html_output
- );
- break;
-
- case 'show':
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/showstories.json?limitToFirst=10&orderBy="$key"',
- 'Show'
- );
- echo_html(
- $GLOBALS['full_domain'] . '/show/',
- 'The latest 30 show stories from Hacker News, proxied by hn.',
- 'hn ~ show',
- $html_output
- );
- break;
-
- case 'job':
- $html_output = get_stories(
- 'https://hacker-news.firebaseio.com/v0/jobstories.json?limitToFirst=10&orderBy="$key"',
- 'Job'
- );
- echo_html(
- $GLOBALS['full_domain'] . '/job/',
- 'The latest 30 job posts from Hacker News, proxied by hn.',
- 'hn ~ job',
- $html_output
- );
- break;
-
- case 'user':
- $html_output = get_user(
- 'https://hacker-news.firebaseio.com/v0/user/' . $elements[0] . '.json',
- 'User Profile: ' . $elements[0]
- );
- echo_html(
- $GLOBALS['full_domain'] . '/user/' . $elements[0],
- 'The Hacker News profile for ' . $elements[0] . ', proxied by hn.',
- 'hn',
- $html_output
- );
- break;
-
- default:
- header('HTTP/1.1 404 Not Found');
- }
-}
-
-/**
- * Extract a set of stories from Hacker News API and format in HTML
- *
- * @access public
- * @param string $api_url The API endpoint to use for extraction
- * @param string $inline_title The <h1> title to use in the HTML
- * @return string $html_output The formatted HTML result of stories from the API
- * @author cmc <hello@cleberg.net>
- */
-function get_stories(string $api_url, string $inline_title): string
-{
- $response_raw = file_get_contents($api_url);
- if ($response_raw == "null") {
- return '<p>ERROR: Stories not found. API returned `null`.</p>';
- } else {
- $response = json_decode($response_raw, true);
- }
-
- $html_output = '<h1>' . $inline_title . '</h1>';
-
- for ($i = 0; $i < count($response); $i++) {
- $sub_url = 'https://hacker-news.firebaseio.com/v0/item/' . $response[$i] . '.json';
- $sub_response_raw = file_get_contents($sub_url);
- $sub_response = json_decode($sub_response_raw, true);
-
- // TODO: Can this be converted to a heredoc string with variables?
- $html = '<div><a href="' . $sub_response['url'] . '">' . $sub_response['title'] . '</a>';
- $html .= '<p><time datetime="' . date('Y-m-d h:m:s', $sub_response['time']) . '">';
- $html .= date('Y-m-d h:m:s', $sub_response['time']) . '</time> by <a';
- $html .= ' href="/user/' . $sub_response['by'] . '">';
- $html .= $sub_response['by'] . '</a> | ' . $sub_response['score'];
- $html .= ' points</p></div>';
- $html_output .= $html;
- }
-
- return $html_output;
-}
-
-/**
- *Extract a user's profile from Hacker News API and format in HTML
- *
- * @access public
- * @param string $api_url The API endpoint to use for extraction
- * @param string $inline_title The <h1> title to use in the HTML
- * @return string $html_output The formatted HTML result of stories from the API
- * @author cmc <hello@cleberg.net>
- */
-function get_user(string $api_url, string $inline_title): string
-{
- $response_raw = file_get_contents($api_url);
- if ($response_raw == "null") {
- return '<p>ERROR: User not found.</p>';
- } else {
- $response = json_decode($response_raw, true);
- }
-
- // TODO: Can this be converted to a heredoc string with variables?
- $html_output = '<h1>' . $inline_title . '</h1>';
- $html_output .= '<p>About: ' . $response['about'] . '</p>';
- $html_output .= '<p>Karma: ' . $response['karma'] . '</p>';
- $html_output .= '<p>Created: <time datetime="' . date('Y-m-d h:m:s', $response['created']) . '>' . date('Y-m-d h:m:s', $response['created']) . '</time></p>';
- $html_output .= '<p>Recently Submitted Posts:</p>';
-
- $limit = count($response['submitted']) > 10 ? 10 : count($response['submitted']);
- if (count($response['submitted']) > 0) {
- for ($i = 0; $i < $limit; $i++) {
- $sub_url = 'https://hacker-news.firebaseio.com/v0/item/' . $response['submitted'][$i] . '.json';
- $sub_response_raw = file_get_contents($sub_url);
- $sub_response = json_decode($sub_response_raw, true);
-
- if ($sub_response['type'] == 'story' || $sub_response['type'] == 'job') {
- $html = '<div><a href="' . $sub_response['url'] . '">' . $sub_response['title'] . '</a>';
- $html .= '<p><time datetime="' . date('Y-m-d h:m:s', $sub_response['time']) . '">';
- $html .= date('Y-m-d h:m:s', $sub_response['time']) . '</time> by <a';
- $html .= ' href="/user/' . $sub_response['by'] . '">';
- $html .= $sub_response['by'] . '</a> | ' . $sub_response['score'];
- $html .= ' points</p></div>';
- } elseif ($sub_response['type'] == 'poll') {
- // TODO: Handle polls
- $html = 'TODO: Add logic to handle polls here.';
- } else {
- // TODO: Add link to parent with $sub_response['parent']
- $html = '<div><time datetime="' . date('Y-m-d h:m:s', $sub_response['time']);
- $html .= '">' . date('Y-m-d h:m:s', $sub_response['time']);
- $html .= '</time><br><p>' . $sub_response['text'] . '</p></div>';
- }
- $html_output .= $html;
- }
- } else {
- $html_output .= '<p>User has no submissions.</p>';
- }
-
- return $html_output;
-}
-
-
-/**
- * Send formatted HTML results to the user via a template
- *
- * @access public
- * @param string $page_url Canonical URL for HTML header
- * @param string $page_description Page description for HTML header
- * @param string $page_title Page title for HTML header
- * @param string $page_content Page content to display in <main>
- * @author cmc <hello@cleberg.net>
- */
-function echo_html(string $page_url, string $page_description, string $page_title, string $page_content)
-{
- include_once 'src/View/class-template.php';
-
- $template = new HN\View\Template(
- $page_url,
- $page_description,
- $page_title,
- $page_content
- );
-
- $template->echo_template('templates/template.html');
-}
-
-// EOF
+$route = new HN\Controllers\RouteController($_SERVER['REQUEST_URI']);
+$route->routeUser();
diff --git a/src/Controller/FeedController.php b/src/Controller/FeedController.php
new file mode 100644
index 0000000..0e3e3b4
--- /dev/null
+++ b/src/Controller/FeedController.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace HN\Controllers;
+
+class FeedController
+{
+ /**
+ * @var string
+ */
+ private string $canonical_url;
+ /**
+ * @var string
+ */
+ private string $description;
+ /**
+ * @var string
+ */
+ private string $title;
+ /**
+ * @var string
+ */
+ private string $content;
+ /**
+ * @var false|string
+ */
+ private mixed $current_year;
+
+ public function __construct(string $canonical_url, string $description, string $title, string $content)
+ {
+ $this->canonical_url = $canonical_url;
+ $this->description = $description;
+ $this->title = $title;
+ $this->content = $content;
+ $this->current_year = date("Y");
+ }
+
+ /**
+ * Request template to be presented to the user
+ *
+ * @access public
+ * @author cmc <hello@cleberg.net>
+ */
+ public function render(): void
+ {
+ include_once 'src/View/BaseTemplate.php';
+ }
+}
diff --git a/src/Controller/RouteController.php b/src/Controller/RouteController.php
new file mode 100644
index 0000000..0d9befa
--- /dev/null
+++ b/src/Controller/RouteController.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace HN\Controllers;
+
+require_once 'src/Controller/FeedController.php';
+
+use function HN\Models\GetApiResults;
+use function HN\Models\ParseItem;
+use function HN\Models\ParseStories;
+use function HN\Models\ParseUser;
+
+class RouteController
+{
+ /**
+ * @var string
+ */
+ private string $request;
+
+ public function __construct(string $request)
+ {
+ $this->request = $request;
+ }
+
+ /**
+ * Route the user to the appropriate function, based on the URL
+ *
+ * @access public
+ * @return void No return type; send user to FeedController->render() or a 404 error
+ * @author cmc <hello@cleberg.net>
+ */
+ public function routeUser(): void
+ {
+ include_once 'src/Model/ApiService.php';
+ $path = ltrim($this->request, '/');
+ $elements = explode('/', $path);
+
+ switch (array_shift($elements)) {
+ case '':
+ case 'top':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'],
+ 'The top stories from Hacker News, proxied by hn.',
+ 'hn',
+ ParseStories(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/topstories.json?limitToFirst=10&orderBy="$key"'
+ ),
+ 'Top'
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'best':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/Best/',
+ 'The best stories from Hacker News, proxied by hn.',
+ 'hn ~ best',
+ ParseStories(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/beststories.json?limitToFirst=10&orderBy="$key"'
+ ),
+ 'Best'
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'new':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/new/',
+ 'The newest stories from Hacker News, proxied by hn.',
+ 'hn ~ new',
+ ParseStories(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/newstories.json?limitToFirst=10&orderBy="$key"'
+ ),
+ 'New'
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'ask':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/ask/',
+ 'The top asks from Hacker News, proxied by hn.',
+ 'hn ~ ask',
+ ParseStories(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/askstories.json?limitToFirst=10&orderBy="$key"'
+ ),
+ 'Ask'
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'show':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/show/',
+ 'The latest showcases from Hacker News, proxied by hn.',
+ 'hn ~ show',
+ ParseStories(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/showstories.json?limitToFirst=10&orderBy="$key"'
+ ),
+ 'Show'
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'job':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/job/',
+ 'The latest jobs from Hacker News, proxied by hn.',
+ 'hn ~ jobs',
+ ParseStories(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/jobstories.json?limitToFirst=10&orderBy="$key"'
+ ),
+ 'Job'
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'user':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/user/' . $elements[0],
+ 'The Hacker News profile for ' . $elements[0] . ', proxied by hn.',
+ 'hn ~ ' . $elements[0],
+ ParseUser(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/user/' . $elements[0] . '.json'
+ ),
+ 'User: ' . $elements[0]
+ )
+ );
+
+ $feed->render();
+ break;
+
+ case 'item':
+ $feed = new FeedController(
+ $GLOBALS['full_domain'] . '/item/' . $elements[0],
+ 'Hacker News story ' . $elements[0] . ', proxied by hn.',
+ 'hn ~ ' . $elements[0],
+ ParseItem(
+ GetApiResults(
+ 'https://hacker-news.firebaseio.com/v0/item/' . $elements[0] . '.json'
+ ),
+ 'Item: ' . $elements[0]
+ )
+ );
+
+ $feed->render();
+ break;
+
+ default:
+ header('HTTP/1.1 404 Not Found');
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Model/ApiService.php b/src/Model/ApiService.php
new file mode 100644
index 0000000..5535c12
--- /dev/null
+++ b/src/Model/ApiService.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace HN\Models;
+
+/**
+ * Extract a set of stories from the Hacker News API
+ *
+ * @access public
+ * @param string $api_url The API endpoint to use for extraction
+ * @return mixed The API results formatted into an HTML section
+ * @author cmc <hello@cleberg.net>
+ */
+function GetApiResults(string $api_url): mixed
+{
+ $response = file_get_contents($api_url);
+ return json_decode($response, true);
+}
+
+/**
+ * Formats a given set of API results into an HTML section
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @param string $inline_title The <h1> title to use in the HTML
+ * @return string $html_output The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ParseStories(mixed $api_results, string $inline_title): string
+{
+ if ($api_results == "null") {
+ return '<p>ERROR: Stories not found. API returned `null`.</p>';
+ } else {
+ $html_output = '<h1>' . $inline_title . '</h1>';
+ for ($i = 0; $i < count($api_results); $i++) {
+ $story_api_results = GetApiResults('https://hacker-news.firebaseio.com/v0/item/' . $api_results[$i] . '.json');
+ $html_output .= ConstructStory($story_api_results);
+ }
+
+ return $html_output;
+ }
+}
+
+/**
+ *Extract a user's profile from Hacker News API and format in HTML
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @param string $inline_title The <h1> title to use in the HTML
+ * @return string $html_output The formatted HTML result of stories from the API
+ * @author cmc <hello@cleberg.net>
+ */
+function ParseUser(mixed $api_results, string $inline_title): string
+{
+ if ($api_results == "null") {
+ return '<p>ERROR: User not found.</p>';
+ } else {
+ $about = $api_results['about'];
+ $karma = $api_results['karma'];
+ $created = date('Y-m-d h:m:s', $api_results['created']);
+
+ $html_output = <<<EOT
+ <div class="user-details">
+ <h1>$inline_title</h1>
+ <p>About: $about</p>
+ <p>Karma: $karma</p>
+ <p>Created: <time datetime="$created">$created</time></p>
+ <br>
+ <h2>Recently Submitted</h2>
+ </div>
+ EOT;
+
+ $limit = (count($api_results['submitted']) > 10) ? 10 : count($api_results['submitted']);
+ if (count($api_results['submitted']) > 0) {
+ for ($i = 0; $i < $limit; $i++) {
+ $user_api_results = GetApiResults('https://hacker-news.firebaseio.com/v0/item/' . $api_results['submitted'][$i] . '.json');
+ $html_output .= GetItem($user_api_results);
+ }
+ } else {
+ $html_output .= '<p>User has no submissions.</p>';
+ }
+
+ return $html_output;
+ }
+}
+
+
+/**
+ * Formats one specific item requested by the user
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @param string $inline_title The <h1> title to use in the HTML
+ * @return string $html_output The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ParseItem(mixed $api_results, string $inline_title): string
+{
+ // TODO: Need to create a page specifially for /item/ requests
+ // TODO: Use the GetItem() and Construct*() functions below, then output in it's own page - possibly with a single parent and descendat, if exist
+ return 'TODO';
+}
+
+
+/**
+ * Formats one item from the API to HTML
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @return string The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function GetItem(mixed $api_results): string
+{
+ $type = $api_results['type'];
+
+ return match ($type) {
+ 'story', 'job' => ConstructStory($api_results),
+ 'comment' => ConstructComment($api_results),
+ 'poll' => ConstructPoll($api_results),
+ 'pollopt' => ConstructPollOpt($api_results),
+ default => 'ERROR: Item type not found.',
+ };
+}
+
+
+/**
+ * Creates a story HTML element
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @return string The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ConstructStory(mixed $api_results): string
+{
+ $id = $api_results['id'];
+ $url = $api_results['url'];
+ $title = $api_results['title'];
+ $time = date('Y-m-d h:m:s', $api_results['time']);
+ $by = $api_results['by'];
+ $score = $api_results['score'];
+ $descendants = $api_results['descendants'];
+
+ return <<<EOT
+ <div class="story">
+ <a href="$url">$title</a>
+ <p>
+ <time datetime="$time">$time</time>
+ by <a href="/user/$by/">$by</a>
+ | $score points
+ | <a href="/item/$id">$descendants comments</a>
+ </p>
+ </div>
+ EOT;
+}
+
+/**
+ * Creates a story discussion page with comments
+ *
+ * @access public
+ * @return string The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ConstructStoryDiscussion(): string
+{
+ // TODO: Create discussion page, using mostly the same details as ConstructStory - except you need to check for $api_results['text'] to see if the poster left text in addition to the link.
+ // : Also need to show comments (at least a list of top level ones to start)
+ return 'TODO';
+}
+
+/**
+ * Creates a comment HTML element
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @return string The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ConstructComment($api_results): string
+{
+ $time = date('Y-m-d h:m:s', $api_results['time']);
+ $text = $api_results['text'];
+ $parent = $api_results['parent'];
+
+ return <<<EOT
+ <div class="comment">
+ <p>$text</p>
+ <p><i>Submitted in response to: <a href="/item/$parent/">$parent</a></i></p>
+ <time datetime="$time">$time</time>
+ </div>
+ EOT;
+}
+
+/**
+ * Creates a poll HTML element
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @return string The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ConstructPoll($api_results): string
+{
+ return 'TODO';
+}
+
+/**
+ * Creates a poll-option HTML element
+ *
+ * @access public
+ * @param mixed $api_results The decoded API results
+ * @return string The formatted HTML result of stories from the API or the error message
+ * @author cmc <hello@cleberg.net>
+ */
+function ConstructPollOpt($api_results): string
+{
+ return 'TODO';
+} \ No newline at end of file
diff --git a/src/Model/CacheService.php b/src/Model/CacheService.php
new file mode 100644
index 0000000..2bf1f57
--- /dev/null
+++ b/src/Model/CacheService.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * TODO: Implement a way to cache certain content in SQLite or something similarly performative
+ *
+ * @access public
+ * @return void
+ * @author cmc <hello@cleberg.net>
+ */
+function CacheService() {} \ No newline at end of file
diff --git a/src/View/BaseTemplate.php b/src/View/BaseTemplate.php
new file mode 100644
index 0000000..17bc39e
--- /dev/null
+++ b/src/View/BaseTemplate.php
@@ -0,0 +1,35 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title><?php echo $this->title; ?></title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="author" content="<?php echo $GLOBALS['author_name']; ?>">
+ <meta name="description" content="<?php echo $this->description; ?>">
+ <link rel="canonical" href="<?php echo $this->canonical_url; ?>">
+ <link rel="stylesheet" href="/static/styles.min.css">
+</head>
+
+<body>
+<main id="main">
+ <nav class="links">
+ <span><a href="/">Top</a> &middot; </span>
+ <span><a href="/best/">Best</a> &middot;</span>
+ <span><a href="/new/">New</a> &middot;</span>
+ <span><a href="/ask/">Ask</a> &middot;</span>
+ <span><a href="/show/">Show</a> &middot;</span>
+ <span><a href="/job/">Job</a></span>
+ </nav>
+ <?php echo $this->content; ?>
+</main>
+
+<footer>
+ <p><a href="https://sr.ht/~cmc/hn/">Source Code</a></p>
+ <p>Copyright &copy; 2023 - <?php echo $this->current_year; ?></p>
+</footer>
+
+</body>
+
+</html>
diff --git a/src/View/class-template.php b/src/View/class-template.php
deleted file mode 100644
index 9a3c598..0000000
--- a/src/View/class-template.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-namespace HN\View;
-
-/**
-* Template View
-*
-* @author cmc <hello@cleberg.net>
-*/
-class Template
-{
- /**
- * @var string
- */
- private $canonical_url;
- /**
- * @var string
- */
- private $description;
- /**
- * @var string
- */
- private $title;
- /**
- * @var string
- */
- private $content;
- /**
- * @var false|string
- */
- private $current_year;
-
- public function __construct(string $canonical_url, string $page_description, string $page_title, string $content_col) {
- $this->canonical_url = $canonical_url;
- $this->description = $page_description;
- $this->title = $page_title;
- $this->content = $content_col;
- $this->current_year = date("Y");
- }
-
- public function echo_template(string $template_file) {
- // Get the template file
- $page = file_get_contents($template_file);
-
- // Replace the template variables
- $page = str_replace('{page_title}', $this->title, $page);
- $page = str_replace('{page_description}', $this->description, $page);
- $page = str_replace('{canonical_url}', $this->canonical_url, $page);
- $page = str_replace('{content}', $this->content, $page);
- $page = str_replace('{current_year}', $this->current_year, $page);
-
- // Echo the filled-out template
- echo $page;
- }
-}
-
-// EOF
-
diff --git a/static/styles.css b/static/styles.css
index 5f847f3..72905ab 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -1,43 +1,184 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
-button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}
+button, hr, input {
+ overflow: visible
+}
+
+progress, sub, sup {
+ vertical-align: baseline
+}
+
+[type=checkbox], [type=radio], legend {
+ box-sizing: border-box;
+ padding: 0
+}
+
+html {
+ line-height: 1.15;
+ -webkit-text-size-adjust: 100%
+}
+
+body {
+ margin: 0
+}
+
+details, main {
+ display: block
+}
+
+h1 {
+ font-size: 2em;
+ margin: .67em 0
+}
+
+hr {
+ box-sizing: content-box;
+ height: 0
+}
+
+code, kbd, pre, samp {
+ font-family: monospace, monospace;
+ font-size: 1em
+}
+
+a {
+ background-color: transparent
+}
+
+abbr[title] {
+ border-bottom: none;
+ text-decoration: underline;
+ text-decoration: underline dotted
+}
+
+b, strong {
+ font-weight: bolder
+}
+
+small {
+ font-size: 80%
+}
+
+sub, sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative
+}
+
+sub {
+ bottom: -.25em
+}
+
+sup {
+ top: -.5em
+}
+
+img {
+ border-style: none
+}
+
+button, input, optgroup, select, textarea {
+ font-family: inherit;
+ font-size: 100%;
+ line-height: 1.15;
+ margin: 0
+}
+
+button, select {
+ text-transform: none
+}
+
+[type=button], [type=reset], [type=submit], button {
+ -webkit-appearance: button
+}
+
+[type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner, button::-moz-focus-inner {
+ border-style: none;
+ padding: 0
+}
+
+[type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring, button:-moz-focusring {
+ outline: ButtonText dotted 1px
+}
+
+fieldset {
+ padding: .35em .75em .625em
+}
+
+legend {
+ color: inherit;
+ display: table;
+ max-width: 100%;
+ white-space: normal
+}
+
+textarea {
+ overflow: auto
+}
+
+[type=number]::-webkit-inner-spin-button, [type=number]::-webkit-outer-spin-button {
+ height: auto
+}
+
+[type=search] {
+ -webkit-appearance: textfield;
+ outline-offset: -2px
+}
+
+[type=search]::-webkit-search-decoration {
+ -webkit-appearance: none
+}
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ font: inherit
+}
+
+summary {
+ display: list-item
+}
+
+[hidden], template {
+ display: none
+}
/* custom css */
body {
- padding: 1rem;
- font-family: system-ui, sans-serif;
+ padding: 1rem;
+ font-family: system-ui, sans-serif;
+ max-width: 40em;
}
body > main > div {
- margin-bottom: 1rem;
+ margin-bottom: 1rem;
}
body > main > div > p {
- margin-top: 0.5rem;
+ margin-top: 0.5rem;
}
a {
- text-decoration: none;
+ text-decoration: none;
}
footer {
- border-top: 1px solid black;
+ border-top: 1px solid black;
}
@media (prefers-color-scheme: dark) {
- body {
- background-color: #000;
- color: #ccc;
- }
-
- h1,h2,h3,h4,h5,h6 {
- color: #fff;
- }
-
- a,a:hover,a:visited {
- color: #0f0;
- }
-
- footer {
- border-color: #ccc;
- }
+ body {
+ background-color: #000;
+ color: #ccc;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ color: #fff;
+ }
+
+ a, a:hover, a:visited {
+ color: #0f0;
+ }
+
+ footer {
+ border-color: #ccc;
+ }
}
diff --git a/static/styles.min.css b/static/styles.min.css
index 3d34535..0328079 100644
--- a/static/styles.min.css
+++ b/static/styles.min.css
@@ -1 +1 @@
-/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:initial}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}body{padding:1rem;font-family:system-ui,sans-serif}body>main>div{margin-bottom:1rem}body>main>div>p{margin-top:.5rem}a{text-decoration:none}footer{border-top:1px solid #000}@media(prefers-color-scheme:dark){body{background-color:#000;color:#ccc}h1,h2,h3,h4,h5,h6{color:#fff}a,a:hover,a:visited{color:#0f0}footer{border-color:#ccc}} \ No newline at end of file
+button, hr, input {overflow: visible }progress, sub, sup {vertical-align: baseline }[type=checkbox], [type=radio], legend {box-sizing: border-box;padding: 0 }html {line-height: 1.15;-webkit-text-size-adjust: 100% }body {margin: 0 }details, main {display: block }h1 {font-size: 2em;margin: .67em 0 }hr {box-sizing: content-box;height: 0 }code, kbd, pre, samp {font-family: monospace, monospace;font-size: 1em }a {background-color: transparent }abbr[title] {border-bottom: none;text-decoration: underline;text-decoration: underline dotted }b, strong {font-weight: bolder }small {font-size: 80% }sub, sup {font-size: 75%;line-height: 0;position: relative }sub {bottom: -.25em }sup {top: -.5em }img {border-style: none }button, input, optgroup, select, textarea {font-family: inherit;font-size: 100%;line-height: 1.15;margin: 0 }button, select {text-transform: none }[type=button], [type=reset], [type=submit], button {-webkit-appearance: button }[type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner, button::-moz-focus-inner {border-style: none;padding: 0 }[type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring, button:-moz-focusring {outline: ButtonText dotted 1px }fieldset {padding: .35em .75em .625em }legend {color: inherit;display: table;max-width: 100%;white-space: normal }textarea {overflow: auto }[type=number]::-webkit-inner-spin-button, [type=number]::-webkit-outer-spin-button {height: auto }[type=search] {-webkit-appearance: textfield;outline-offset: -2px }[type=search]::-webkit-search-decoration {-webkit-appearance: none }::-webkit-file-upload-button {-webkit-appearance: button;font: inherit }summary {display: list-item }[hidden], template {display: none }body {padding: 1rem;font-family: system-ui, sans-serif;max-width: 40em;}body > main > div {margin-bottom: 1rem;}body > main > div > p {margin-top: 0.5rem;}a {text-decoration: none;}footer {border-top: 1px solid black;}.user-submission {border-bottom: 1px solid black;}@media (prefers-color-scheme: dark) {body {background-color: #000;color: #ccc;}h1, h2, h3, h4, h5, h6 {color: #fff;}a, a:hover, a:visited {color: #0f0;}footer {border-color: #ccc;}.user-submission {border-color: white;}} \ No newline at end of file
diff --git a/templates/template.html b/templates/template.html
deleted file mode 100644
index 51b24b7..0000000
--- a/templates/template.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!doctype html>
-<html lang="en">
-
-<head>
- <title>{page_title}</title>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <meta http-equiv="x-ua-compatible" content="ie=edge">
- <meta name="author" content="My Name">
- <meta name="description" content="{page_description}">
- <link rel="canonical" href="{canonical_url}">
- <!--<link rel="icon" href="/favicon.ico">-->
- <link rel="stylesheet" href="/static/styles.min.css">
-</head>
-
-<body>
- <main id="main">
- <nav class="links">
- <span><a href="/">Top</a> &middot; </span>
- <span><a href="/best/">Best</a> &middot;</span>
- <span><a href="/new/">New</a> &middot;</span>
- <span><a href="/ask/">Ask</a> &middot;</span>
- <span><a href="/show/">Show</a> &middot;</span>
- <span><a href="/job/">Job</a></span>
- </nav>
- {content}
- </main>
-
- <footer>
- <p><a href="https://sr.ht/~cmc/hn/">Source Code</a></p>
- <p>Copyright &copy; 2023 - {current_year}</p>
- </footer>
-
-</body>
-
-</html>