diff options
author | Christian Cleberg <hello@cleberg.net> | 2023-06-14 22:08:34 -0500 |
---|---|---|
committer | Christian Cleberg <hello@cleberg.net> | 2023-06-14 22:08:34 -0500 |
commit | 49efb238b879bce764d04dad99c7a169e80f93dd (patch) | |
tree | 7b1d26dbe86710ee7830b3fe6ff82afff3712e2d | |
parent | 53488151f29e3afbd1b1348597ff2123b97ad2d7 (diff) | |
download | hn-49efb238b879bce764d04dad99c7a169e80f93dd.tar.gz hn-49efb238b879bce764d04dad99c7a169e80f93dd.tar.bz2 hn-49efb238b879bce764d04dad99c7a169e80f93dd.zip |
massive overhaul to implement proper MVC
-rw-r--r-- | CODE_OF_CONDUCT.md | 4 | ||||
-rw-r--r-- | CONTRIBUTING.md | 4 | ||||
-rw-r--r-- | README.md | 34 | ||||
-rw-r--r-- | index.php | 238 | ||||
-rw-r--r-- | src/Controller/FeedController.php | 47 | ||||
-rw-r--r-- | src/Controller/RouteController.php | 171 | ||||
-rw-r--r-- | src/Model/ApiService.php | 218 | ||||
-rw-r--r-- | src/Model/CacheService.php | 10 | ||||
-rw-r--r-- | src/View/BaseTemplate.php | 35 | ||||
-rw-r--r-- | src/View/class-template.php | 58 | ||||
-rw-r--r-- | static/styles.css | 187 | ||||
-rw-r--r-- | static/styles.min.css | 2 | ||||
-rw-r--r-- | templates/template.html | 36 |
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. @@ -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. @@ -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> · </span> + <span><a href="/best/">Best</a> ·</span> + <span><a href="/new/">New</a> ·</span> + <span><a href="/ask/">Ask</a> ·</span> + <span><a href="/show/">Show</a> ·</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 © 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> · </span> - <span><a href="/best/">Best</a> ·</span> - <span><a href="/new/">New</a> ·</span> - <span><a href="/ask/">Ask</a> ·</span> - <span><a href="/show/">Show</a> ·</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 © 2023 - {current_year}</p> - </footer> - -</body> - -</html> |