diff options
Diffstat (limited to 'src')
-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 |
6 files changed, 481 insertions, 58 deletions
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 - |