From 49efb238b879bce764d04dad99c7a169e80f93dd Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 14 Jun 2023 22:08:34 -0500 Subject: massive overhaul to implement proper MVC --- CODE_OF_CONDUCT.md | 4 +- CONTRIBUTING.md | 4 +- README.md | 34 +++--- index.php | 238 +------------------------------------ src/Controller/FeedController.php | 47 ++++++++ src/Controller/RouteController.php | 171 ++++++++++++++++++++++++++ src/Model/ApiService.php | 218 +++++++++++++++++++++++++++++++++ src/Model/CacheService.php | 10 ++ src/View/BaseTemplate.php | 35 ++++++ src/View/class-template.php | 58 --------- static/styles.css | 187 +++++++++++++++++++++++++---- static/styles.min.css | 2 +- templates/template.html | 36 ------ 13 files changed, 674 insertions(+), 370 deletions(-) create mode 100644 src/Controller/FeedController.php create mode 100644 src/Controller/RouteController.php create mode 100644 src/Model/ApiService.php create mode 100644 src/Model/CacheService.php create mode 100644 src/View/BaseTemplate.php delete mode 100644 src/View/class-template.php delete mode 100644 templates/template.html 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 @@ title to use in the HTML - * @return string $html_output The formatted HTML result of stories from the API - * @author cmc - */ -function get_stories(string $api_url, string $inline_title): string -{ - $response_raw = file_get_contents($api_url); - if ($response_raw == "null") { - return '

ERROR: Stories not found. API returned `null`.

'; - } else { - $response = json_decode($response_raw, true); - } - - $html_output = '

' . $inline_title . '

'; - - 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 = '
' . $sub_response['title'] . ''; - $html .= '

by '; - $html .= $sub_response['by'] . ' | ' . $sub_response['score']; - $html .= ' points

'; - $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

title to use in the HTML - * @return string $html_output The formatted HTML result of stories from the API - * @author cmc - */ -function get_user(string $api_url, string $inline_title): string -{ - $response_raw = file_get_contents($api_url); - if ($response_raw == "null") { - return '

ERROR: User not found.

'; - } else { - $response = json_decode($response_raw, true); - } - - // TODO: Can this be converted to a heredoc string with variables? - $html_output = '

' . $inline_title . '

'; - $html_output .= '

About: ' . $response['about'] . '

'; - $html_output .= '

Karma: ' . $response['karma'] . '

'; - $html_output .= '

Created:

by '; - $html .= $sub_response['by'] . ' | ' . $sub_response['score']; - $html .= ' points

'; - } 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 = '

' . $sub_response['text'] . '

'; - } - $html_output .= $html; - } - } else { - $html_output .= '

User has no submissions.

'; - } - - 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
- * @author cmc - */ -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 @@ +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 + */ + 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 @@ +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 + */ + 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 @@ + + */ +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

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 + */ +function ParseStories(mixed $api_results, string $inline_title): string +{ + if ($api_results == "null") { + return '

ERROR: Stories not found. API returned `null`.

'; + } else { + $html_output = '

' . $inline_title . '

'; + 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

title to use in the HTML + * @return string $html_output The formatted HTML result of stories from the API + * @author cmc + */ +function ParseUser(mixed $api_results, string $inline_title): string +{ + if ($api_results == "null") { + return '

ERROR: User not found.

'; + } else { + $about = $api_results['about']; + $karma = $api_results['karma']; + $created = date('Y-m-d h:m:s', $api_results['created']); + + $html_output = << +

$inline_title

+

About: $about

+

Karma: $karma

+

Created:

+
+

Recently Submitted

+ + 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 .= '

User has no submissions.

'; + } + + 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

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 + */ +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 + */ +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 + */ +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 << + $title +

+ + by $by + | $score points + | $descendants comments +

+ + 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 + */ +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 + */ +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 << +

$text

+

Submitted in response to: $parent

+ + + 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 + */ +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 + */ +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 @@ + + */ +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 @@ + + + + + <?php echo $this->title; ?> + + + + + + + + + + +
+ + content; ?> +
+ +
+

Source Code

+

Copyright © 2023 - current_year; ?>

+
+ + + + 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 @@ - -*/ -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 @@ - - - - - {page_title} - - - - - - - - - - - -
- - {content} -
- - - - - - -- cgit v1.2.3-70-g09d2