├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── config │ └── config.json ├── routes │ └── routes.js └── views │ ├── 404.ward.html │ └── home.ward.html ├── core ├── App.js ├── init │ ├── config.js │ └── index.js ├── ressources │ └── core.css ├── routing │ ├── Route.js │ └── Router.js ├── templating │ ├── Component.js │ ├── Engine.js │ ├── View.js │ └── helpers.js └── utility │ └── load.js ├── package-lock.json ├── package.json ├── procfile ├── public ├── assets │ ├── css │ │ ├── app.css │ │ └── app.min.css │ └── images │ │ └── logo.png └── index.html └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | # Yarn Integrity file 16 | .yarn-integrity -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Colin Espinas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### The full documentation is not available for now, the project is still in early stages. 3 | 4 | 5 |
6 |

7 | 8 | Logo 9 | 10 | 11 |

WARD

12 | 13 |

14 | A fast & simple client side framework for building websites 15 |
16 | Explore the docs » 17 |
18 |
19 | View Demo 20 | · 21 | Report Bug 22 | · 23 | Request Feature 24 |
25 |
26 | 27 | Contributors 28 | 29 | 30 | Forks 31 | 32 | 33 | Stargazer 34 | 35 | 36 | Issues 37 | 38 | 39 | License 40 | 41 | 42 | Linkedin 43 | 44 |
45 |
46 |
47 |

48 |

49 | 50 | 51 | 52 | 53 | ## Table of Contents 54 | 55 | * [About](#about) 56 | * [Getting Started](#getting-started) 57 | * [Prerequisites](#prerequisites) 58 | * [Installation](#installation) 59 | * [Usage](#usage) 60 | * [Templating](#templating) 61 | * [Views](#views) 62 | * [Template Engine](#template-engine) 63 | * [Components](#components) 64 | * [Routing](#routing) 65 | * [Contributing](#contributing) 66 | * [License](#license) 67 | * [Contact](#contact) 68 | 69 | 70 | 71 | 72 | 73 | ## About 74 | 75 | [![Product Name Screen Shot][product-screenshot]](https://ward-demo.herokuapp.com/) 76 | 77 | Ward is a simple client sided framework that helps you create fast websites. It is really easy to use and comes with routing and templating. 78 | 79 | 80 | 81 | 82 | ## Getting Started 83 | 84 | Get your Ward project up and ready. 85 | 86 | ### Prerequisites 87 | 88 | Ward is a standalone framework, you don't need anything to make it work but you will need Node.js and a package manager to serve it easely with [ward-server](https://github.com/ward-framework/ward-server). 89 | 90 | ### Installation 91 | 92 | #### CLI 93 | 94 | It is recommended to use the [ward-cli](https://github.com/ward-framework/ward-cli) to create and serve your Ward projects 95 | 1. Install ward-cli globally: 96 | ```sh 97 | npm install ward-cli -g 98 | ``` 99 | 100 | 2. Create a new Ward project and serve it: 101 | ```sh 102 | # Create a new project 103 | ward new MyProject 104 | # Move into the project directory 105 | cd MyProject 106 | # Serve the project 107 | ward serve 108 | ``` 109 | 110 | #### Git Clone 111 | 112 | 1. This repository can act as a skeleton for Ward projects so you just need to clone it: 113 | ```sh 114 | git clone https://github.com/ColinEspinas/ward.git 115 | ``` 116 | 2. Install dependencies ([ward-server](https://github.com/ward-framework/ward-server)): 117 | ```sh 118 | npm install 119 | ``` 120 | 3. Now if you want to serve your Ward project use: 121 | ```sh 122 | npm start 123 | ``` 124 | 125 | NOTE: Nothing stops you from serving Ward projects with Apache servers by tweeking your `.htaccess` file. If you do so, do not hesitate to share your methods to help the development of the project. 126 | 127 | 128 | ## Usage 129 | 130 | The content below is just explaining basic usages, consider cheking the [documentation]() about more specific use cases. 131 | 132 | ### Templating 133 | 134 | #### Views 135 | 136 | Ward uses view loading to display content on routes. Views uses the `.ward.html` extension. 137 | 138 | Views are defined by a head and a body like normal html pages: 139 | ```html 140 | 141 | View title 142 | 143 | 144 | 145 |

View content

146 |
147 | ``` 148 | 149 | The name of a view will be the path of the view from the `app/views` folder without the extension: 150 | ``` 151 | "app/views/myhomepage.ward.html" => "myhomepage" 152 | "app/views/mypages/myhomepage.ward.html" => "mypages/myhomepage" 153 | ``` 154 | 155 | In javascript the View object is constructed with: 156 | ```javascript 157 | View(name, params/*optional*/)); 158 | ``` 159 | 160 | 161 | #### Template engine 162 | 163 | The Ward templating engine works with `{# expression #}` and allows every javascript expression. 164 | 165 | You can use it to pass parameters to a view: 166 | ```html 167 |

{# name #}

168 | ``` 169 | NOTE: You can pass any javascript global variable too. 170 | 171 | And you can also use logic structures and functions: 172 | ```html 173 | 174 | {# for(let item of items) { #} 175 |

{# item.name #}

176 | {# console.log(item) #} 177 | {# } #} 178 |
179 | ``` 180 | 181 | 182 | #### Components 183 | 184 | Components are just like views, they use the extension `ward.html` but are self contained and can be included in views or in other components. 185 | 186 | Like views, the name of a component will be the path of the component from the `app/views` folder without the extension: 187 | ``` 188 | "app/views/components/button.ward.html" => "compontents/button" 189 | ``` 190 | 191 | In javascript the Component object is constructed with: 192 | ```javascript 193 | Component(tag, name/*optional*/, params/*optional*/)); 194 | ``` 195 | 196 | To add a component to a view you need to use the `component` helper function: 197 | ```html 198 | {# component("tag", "name", { options : true }) #} 199 | ``` 200 | 201 | That helper function shares the same arguments as the Component object constructor. 202 | 203 | 204 | ### Routing 205 | Ward uses hash navigation by default, that means that your URI will look like `/#/this/is/a/route`. 206 | 207 | The routing is done in `app/routes/routes.js`. 208 | 209 | To register a new route you should use: 210 | ```javascript 211 | router.register(new Route(path, callback)); 212 | ``` 213 | ```javascript 214 | // Exemple: 215 | router.register(new Route("/home", function() { 216 | return new View("home"); 217 | })); 218 | ``` 219 | Routes that return Views will load that view, you can pass arguments to a view with: 220 | ```javascript 221 | return new View(name, arguments); 222 | ``` 223 | 224 | To navigate to a new route use: 225 | ```javascript 226 | router.redirect(path); 227 | ``` 228 | ```javascript 229 | // Exemple: 230 | router.redirect("/home"); 231 | ``` 232 | 233 | To get the right path to a view use: 234 | ```javascript 235 | Route.link(path); // In javascript 236 | link(path) // Helper for templates 237 | ``` 238 | ```html 239 | 240 | Home 241 | ``` 242 | 243 | To add a name to a route use: 244 | ```javascript 245 | route.name("home"); 246 | ``` 247 | 248 | To create an alias to a route use: 249 | ```javascript 250 | route.alias("/path"); 251 | ``` 252 | 253 | You can chain the `name` and `alias` method like this: 254 | ```javascript 255 | router.register(new Route("/home", function() { 256 | return new View("home"); 257 | }).name("home").alias("/myhomepage")); 258 | ``` 259 | 260 | 261 | 262 | ## Contributing 263 | 264 | This project is developed by a somewhat beginner javascript developer, help is always welcome. Do not hesitate to contribute to the project. 265 | 266 | 1. Fork the Project 267 | 2. Create your Feature or Fix Branch (`git checkout -b feature/Feature` or `git checkout -b fix/Fix`) 268 | 3. Commit your Changes (`git commit -m 'Add some feature or fix'`) 269 | 4. Push to the Branch (`git push origin feature/Feature` or `git push origin fix/Fix`) 270 | 5. Open a Pull Request 271 | 272 | 273 | 274 | 275 | ## License 276 | 277 | Ward is distributed under the MIT License. See `LICENSE` for more information. 278 | 279 | 280 | 281 | 282 | ## Contact 283 | 284 | Colin Espinas - [Website](https://colinespinas.com) - contact@colinespinas.com 285 | 286 | Project link: [https://github.com/ColinEspinas/ward](https://github.com/ward-framework/ward) 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /app/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "DEV", 3 | "route": { 4 | "type": "hash" 5 | }, 6 | "path": { 7 | "dir": "/ward/public/", 8 | "app": "../app/", 9 | "public": "../public/", 10 | "views": "../app/views/", 11 | "styles": "../public/assets/css/", 12 | "scripts": "../public/assets/js/" 13 | } 14 | } -------------------------------------------------------------------------------- /app/routes/routes.js: -------------------------------------------------------------------------------- 1 | import App from "../../core/App.js"; 2 | import Route from "../../core/routing/Route.js"; 3 | import View from "../../core/templating/View.js"; 4 | 5 | const router = App.get.router; 6 | 7 | // Register routes here 8 | router.register(new Route("/", function() { return new View("home"); }).name("home")); -------------------------------------------------------------------------------- /app/views/404.ward.html: -------------------------------------------------------------------------------- 1 | 2 | Page not found 3 | 4 | 5 | 6 |
7 |

404

8 |

Oops, this route does not exist 😕

9 | 12 |
13 |
-------------------------------------------------------------------------------- /app/views/home.ward.html: -------------------------------------------------------------------------------- 1 | 2 | Welcome! 3 | 4 | 5 | 6 |
7 |

WARD

8 |

You are now ready to start your website 👍

9 | 13 |
14 |
-------------------------------------------------------------------------------- /core/App.js: -------------------------------------------------------------------------------- 1 | import { config } from "./init/config.js"; 2 | import Router from "./routing/Router.js"; 3 | import { OnConfigLoaded } from "./utility/load.js"; 4 | 5 | const app = Symbol(); 6 | const singletonEnforcer = Symbol(); 7 | 8 | class App { 9 | constructor(enforcer) { 10 | if (enforcer !== singletonEnforcer) { 11 | throw new Error('Cannot construct App'); 12 | } 13 | 14 | OnConfigLoaded(function() { 15 | // Define app router 16 | const options = config.mode === "DEV" ? {muted:false} : {muted:true}; 17 | this.router = new Router([], options); 18 | 19 | // Send "AppLoaded" event to window 20 | let event = new Event("AppLoaded"); 21 | window.dispatchEvent(event); 22 | }.bind(this)); 23 | } 24 | 25 | static get get() { 26 | if (!this[app]) { 27 | this[app] = new App(singletonEnforcer); 28 | } 29 | 30 | return this[app]; 31 | } 32 | } 33 | 34 | export default App; -------------------------------------------------------------------------------- /core/init/config.js: -------------------------------------------------------------------------------- 1 | import { loadJSON } from "../utility/load.js"; 2 | 3 | // Config location path (from index.html) 4 | const configPath = '../../app/config/config.json'; 5 | 6 | // Global config variable 7 | let config; 8 | 9 | // Load config file into config object variable 10 | function loadConfig() { 11 | loadJSON(configPath, function(response) { 12 | config = JSON.parse(response); 13 | 14 | // Send "ConfigLoaded" event to window 15 | let event = new Event("ConfigLoaded"); 16 | window.dispatchEvent(event); 17 | }); 18 | } 19 | 20 | export { config, loadConfig }; -------------------------------------------------------------------------------- /core/init/index.js: -------------------------------------------------------------------------------- 1 | import { config, loadConfig } from "./config.js"; 2 | import App from "../App.js"; 3 | import { OnAppLoaded, OnReady } from "../utility/load.js"; 4 | 5 | const app = App.get; 6 | 7 | // Init function called Onload 8 | function init() { 9 | 10 | // Load config on start 11 | loadConfig(); 12 | // When config is loaded, init routes methods 13 | OnAppLoaded(function() { 14 | 15 | import("../../app/routes/routes.js").then(() => { 16 | // Change route on hash change 17 | if (config.route.type === "hash") { 18 | window.addEventListener('hashchange', function() { 19 | app.router.redirect(app.router.location()); 20 | }); 21 | } 22 | 23 | // Load page if hash not changed (Direct link) 24 | app.router.redirect(app.router.location()); 25 | }); 26 | }); 27 | } 28 | 29 | // Add init to onload listener 30 | OnReady(init); -------------------------------------------------------------------------------- /core/ressources/core.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | viewhead { 6 | display: none; 7 | } 8 | 9 | viewbody { 10 | margin: 0; 11 | display: block; 12 | } -------------------------------------------------------------------------------- /core/routing/Route.js: -------------------------------------------------------------------------------- 1 | import { config } from "../init/config.js"; 2 | import App from "../App.js"; 3 | import View from "../templating/View.js"; 4 | 5 | class Route { 6 | constructor(path, callback) { 7 | this.path = path; 8 | this.params = {}; 9 | this.aliases = []; 10 | if (config && config.route.type === "hash") { 11 | if (this.path) { 12 | this.path = this.path.replace("/", "#/"); 13 | } 14 | 15 | if (this.path === "#/") { 16 | this.alias(""); 17 | } 18 | } 19 | this.callback = callback; 20 | this.router; 21 | this.name; 22 | } 23 | 24 | // Check route callback and handle it 25 | check() { 26 | let routeReturn = this.callback.call(); 27 | 28 | // If view is returned load it 29 | if (routeReturn instanceof View) { 30 | routeReturn.load(); 31 | } 32 | } 33 | 34 | static link(path) { 35 | if (config && config.route.type === "hash") { 36 | if (path) { 37 | path = path.replace("/", "#/"); 38 | } 39 | } 40 | 41 | for (const route of App.get.router.routes) { 42 | if (route.name === path) { 43 | return route.path; 44 | } 45 | if (route.path === path) { 46 | return route.path; 47 | } 48 | else { 49 | for(const alias of route.aliases) { 50 | if (alias === path) { 51 | return alias; 52 | } 53 | } 54 | } 55 | } 56 | return this.link("/notfound"); 57 | } 58 | 59 | alias(path) { 60 | if (config && config.route.type === "hash") { 61 | if (path) { 62 | path = path.replace("/", "#/"); 63 | } 64 | 65 | if (path === "/") { 66 | this.alias(""); 67 | } 68 | } 69 | this.aliases.push(path); 70 | return this; // Return the route for chaining 71 | } 72 | 73 | name(name) { 74 | this.name = name; 75 | return this; // Return the route for chaining 76 | } 77 | } 78 | 79 | export default Route; -------------------------------------------------------------------------------- /core/routing/Router.js: -------------------------------------------------------------------------------- 1 | import { config } from "../init/config.js"; 2 | import Route from "./Route.js"; 3 | import View from "../templating/View.js"; 4 | 5 | class Router { 6 | constructor(routes, options) { 7 | if (options && Object.entries(options).length >= 0 && options.constructor === Object) { 8 | this.options = options; 9 | } else { 10 | this.options = { 11 | muted: true, // If false message at each redirect. 12 | }; 13 | } 14 | 15 | if (routes) { 16 | this.routes = routes; 17 | } else { 18 | this.routes = []; 19 | } 20 | 21 | this.current; 22 | 23 | this.routes.push(new Route("/notfound", function () { 24 | return new View("404"); 25 | })); 26 | } 27 | 28 | // Register a route to router 29 | register(route) { 30 | this.routes.push(route); 31 | route.router = this; 32 | } 33 | 34 | // Get current location hash 35 | location() { 36 | if (config && config.route.type === "hash") { 37 | return window.location.hash; 38 | } 39 | } 40 | 41 | setLocation(path) { 42 | if (config && config.route.type === "hash") { 43 | window.location.hash = path; 44 | } 45 | } 46 | 47 | // Check route on path 48 | redirect(path) { 49 | if (!this.current || path !== this.current.path) { 50 | let found = false; 51 | for (let route of this.routes) { 52 | if (route.path === path) { 53 | found = true; 54 | 55 | // Check route 56 | route.check(); 57 | this.current = route; 58 | this.setLocation(path); 59 | 60 | // Send "redirected" Event when redirect is successful 61 | var event = new CustomEvent('redirected', { 62 | detail: { 63 | route: this.current 64 | } 65 | }); 66 | document.dispatchEvent(event); 67 | 68 | // Send console message if not muted 69 | if (!this.options.muted) { 70 | console.log("Redirected from router :", this); 71 | } 72 | } 73 | else { 74 | for (let alias of route.aliases) { 75 | if (alias === path) { 76 | found = true; 77 | 78 | // Check route 79 | route.check(); 80 | this.current = route; 81 | this.setLocation(this.current.path); 82 | 83 | // Send "redirected" Event when redirect is successful 84 | var event = new CustomEvent('redirected', { 85 | detail: { 86 | route: this.current 87 | } 88 | }); 89 | document.dispatchEvent(event); 90 | 91 | // Send console message if not muted 92 | if (!this.options.muted) { 93 | console.log("Redirected from router :", this); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | // Send "not found" message if not muted 101 | if (!found && !this.options.muted) { 102 | console.log("Cannot find route in routes :", path, this.routes); 103 | this.redirect(Route.link("/notfound")); 104 | } 105 | } 106 | } 107 | } 108 | 109 | export default Router; -------------------------------------------------------------------------------- /core/templating/Component.js: -------------------------------------------------------------------------------- 1 | import { config } from "../init/config.js"; 2 | import templateEngine from "./Engine.js"; 3 | 4 | class Component { 5 | constructor(tag, name, params) { 6 | this.tag = tag; 7 | this.name = name; 8 | this.path = `${config.path.views}${this.name}.ward.html`; 9 | if (params) { 10 | this.params = params; 11 | } 12 | else { 13 | this.params = {}; 14 | } 15 | this.content = ""; 16 | } 17 | 18 | // Load parsed template into view 19 | load() { 20 | let params = this.params; 21 | let path = this.path; 22 | let component = this; 23 | let xhr = new XMLHttpRequest(); 24 | xhr.overrideMimeType("text/html"); 25 | xhr.open('GET', this.path, true); 26 | xhr.onreadystatechange = function () { 27 | if (xhr.readyState == 4 && xhr.status == "200") { 28 | 29 | // Parse view template and adds it to the document 30 | document.querySelector(component.tag).innerHTML = templateEngine.parse(this.responseText, params); 31 | } 32 | if (this.status != 200) { 33 | throw Error("Cannot load component from " + path) 34 | } 35 | }; 36 | xhr.send(null); 37 | return document.createElement(this.tag).outerHTML; 38 | } 39 | 40 | // Load raw html into view 41 | loadRaw() { 42 | let xhr = new XMLHttpRequest(); 43 | xhr.overrideMimeType("text/html"); 44 | xhr.open('GET', this.path, true); 45 | xhr.onreadystatechange = function () { 46 | if (xhr.readyState == 4 && xhr.status == "200") { 47 | // Adds raw content to page 48 | document.querySelector(component.tag).innerHTML = this.responseText; 49 | } 50 | }; 51 | xhr.send(null); 52 | } 53 | } 54 | 55 | export default Component; -------------------------------------------------------------------------------- /core/templating/Engine.js: -------------------------------------------------------------------------------- 1 | import getHelpers from "./helpers.js"; 2 | 3 | class templateEngine { 4 | 5 | // Parse template (content) with parameters 6 | static parse(content, params) { 7 | let scope = getHelpers(params); 8 | let re = /{#(.+?)#}/g, 9 | reExp = /(^\s*(if|for|else|switch|case|break|{|})).*/g, 10 | code = 'with(obj) { var r=[];\n', 11 | cursor = 0, 12 | result, 13 | match; 14 | let add = function (line, js) { 15 | js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') : 16 | (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''); 17 | return add; 18 | } 19 | while (match = re.exec(content)) { 20 | add(content.slice(cursor, match.index))(match[1], true); 21 | cursor = match.index + match[0].length; 22 | } 23 | add(content.substr(cursor, content.length - cursor)); 24 | code = (code + 'return r.join(""); }').replace(/[\r\t\n]/g, ' '); 25 | try { 26 | result = new Function('obj', code).apply(scope, [params]); 27 | } 28 | catch (err) { 29 | console.error(err.message, "falling back to raw content."); 30 | return content; 31 | } 32 | return result; 33 | }; 34 | }; 35 | 36 | export default templateEngine; -------------------------------------------------------------------------------- /core/templating/View.js: -------------------------------------------------------------------------------- 1 | import { config } from "../init/config.js"; 2 | import templateEngine from "./Engine.js"; 3 | 4 | class View { 5 | constructor(name, params) { 6 | this.name = name; 7 | this.path = `${config.path.views}${this.name}.ward.html`; 8 | if (params) { 9 | this.params = params; 10 | } 11 | else { 12 | this.params = {}; 13 | } 14 | } 15 | 16 | // Load parsed template view into "viewroot" 17 | load() { 18 | let params = this.params; 19 | let xhr = new XMLHttpRequest(); 20 | xhr.overrideMimeType("text/html"); 21 | xhr.open('GET', this.path, true); 22 | xhr.onreadystatechange = function () { 23 | if (xhr.readyState == 4 && xhr.status == "200") { 24 | 25 | // Parse view template 26 | let parsedview = templateEngine.parse(this.responseText, params); 27 | 28 | // Load parsed html into viewroot element 29 | document.querySelector("viewroot").innerHTML = parsedview; 30 | 31 | // Change page title with view title 32 | let title = document.querySelector("viewhead").querySelector("title"); 33 | document.title = title.text; 34 | } 35 | }; 36 | xhr.send(null); 37 | } 38 | 39 | // Load raw html view into "viewroot" 40 | loadRaw() { 41 | let xhr = new XMLHttpRequest(); 42 | xhr.overrideMimeType("text/html"); 43 | xhr.open('GET', this.path, true); 44 | xhr.onreadystatechange = function () { 45 | if (xhr.readyState == 4 && xhr.status == "200") { 46 | 47 | // Load html into viewroot element 48 | document.querySelector("viewroot").innerHTML = this.responseText; 49 | 50 | // Change page title with view title 51 | let title = document.querySelector("viewhead").querySelector("title"); 52 | document.title = title.text; 53 | } 54 | }; 55 | xhr.send(null); 56 | } 57 | } 58 | 59 | export default View; -------------------------------------------------------------------------------- /core/templating/helpers.js: -------------------------------------------------------------------------------- 1 | import Route from "../routing/Route.js"; 2 | import Component from "./Component.js"; 3 | import App from "../App.js"; 4 | 5 | function helpers(params) { 6 | 7 | let scope = params; 8 | 9 | // Helpers are registered here: 10 | 11 | 12 | // Get correct route path from url 13 | scope.link = function(url) { 14 | return Route.link(url); 15 | } 16 | 17 | // Get current location into a string 18 | scope.location = function() { 19 | return App.get.router.location(); 20 | } 21 | 22 | // Instantiate a new component 23 | scope.component = function(tag, name, params) { 24 | 25 | let component; 26 | 27 | if (typeof name !== 'string') { 28 | params = name; 29 | name = tag; 30 | } 31 | 32 | if (typeof params === 'object' && params !== null) { 33 | component = new Component(tag, name, params); 34 | } 35 | else { 36 | component = new Component(tag, name); 37 | } 38 | 39 | return component.load(); 40 | } 41 | 42 | 43 | 44 | 45 | 46 | 47 | return scope; 48 | } 49 | 50 | export default helpers; -------------------------------------------------------------------------------- /core/utility/load.js: -------------------------------------------------------------------------------- 1 | // Add func to "DOMContentLoaded" event queue 2 | function OnReady(func) { 3 | window.addEventListener("DOMContentLoaded", func); 4 | } 5 | // Add func to "load" event queue 6 | function OnLoad(func) { 7 | window.addEventListener("load", func); 8 | } 9 | 10 | function OnConfigLoaded(func) { 11 | window.addEventListener('ConfigLoaded', func); 12 | } 13 | 14 | function OnAppLoaded(func) { 15 | window.addEventListener('AppLoaded', func); 16 | } 17 | 18 | function loadJSON(path, callback, err) { 19 | let xhr = new XMLHttpRequest(); 20 | xhr.overrideMimeType("application/json"); 21 | xhr.open('GET', path, true); 22 | xhr.onreadystatechange = function () { 23 | if (xhr.readyState == 4 && xhr.status == "200") { 24 | callback(xhr.responseText); 25 | } 26 | else { 27 | if (err) 28 | err(); 29 | } 30 | }; 31 | xhr.send(null); 32 | } 33 | 34 | export { OnReady, OnLoad, OnConfigLoaded, OnAppLoaded, loadJSON }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ward-project", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/color-name": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 10 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" 11 | }, 12 | "ansi-styles": { 13 | "version": "4.2.1", 14 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 15 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 16 | "requires": { 17 | "@types/color-name": "^1.1.1", 18 | "color-convert": "^2.0.1" 19 | } 20 | }, 21 | "chalk": { 22 | "version": "3.0.0", 23 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", 24 | "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", 25 | "requires": { 26 | "ansi-styles": "^4.1.0", 27 | "supports-color": "^7.1.0" 28 | } 29 | }, 30 | "color-convert": { 31 | "version": "2.0.1", 32 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 33 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 34 | "requires": { 35 | "color-name": "~1.1.4" 36 | } 37 | }, 38 | "color-name": { 39 | "version": "1.1.4", 40 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 41 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 42 | }, 43 | "connect": { 44 | "version": "3.7.0", 45 | "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", 46 | "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", 47 | "requires": { 48 | "debug": "2.6.9", 49 | "finalhandler": "1.1.2", 50 | "parseurl": "~1.3.3", 51 | "utils-merge": "1.0.1" 52 | } 53 | }, 54 | "debug": { 55 | "version": "2.6.9", 56 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 57 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 58 | "requires": { 59 | "ms": "2.0.0" 60 | } 61 | }, 62 | "depd": { 63 | "version": "1.1.2", 64 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 65 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 66 | }, 67 | "destroy": { 68 | "version": "1.0.4", 69 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 70 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 71 | }, 72 | "ee-first": { 73 | "version": "1.1.1", 74 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 75 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 76 | }, 77 | "encodeurl": { 78 | "version": "1.0.2", 79 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 80 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 81 | }, 82 | "escape-html": { 83 | "version": "1.0.3", 84 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 85 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 86 | }, 87 | "etag": { 88 | "version": "1.8.1", 89 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 90 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 91 | }, 92 | "finalhandler": { 93 | "version": "1.1.2", 94 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 95 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 96 | "requires": { 97 | "debug": "2.6.9", 98 | "encodeurl": "~1.0.2", 99 | "escape-html": "~1.0.3", 100 | "on-finished": "~2.3.0", 101 | "parseurl": "~1.3.3", 102 | "statuses": "~1.5.0", 103 | "unpipe": "~1.0.0" 104 | } 105 | }, 106 | "fresh": { 107 | "version": "0.5.2", 108 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 109 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 110 | }, 111 | "has-flag": { 112 | "version": "4.0.0", 113 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 114 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 115 | }, 116 | "http-errors": { 117 | "version": "1.7.3", 118 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", 119 | "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", 120 | "requires": { 121 | "depd": "~1.1.2", 122 | "inherits": "2.0.4", 123 | "setprototypeof": "1.1.1", 124 | "statuses": ">= 1.5.0 < 2", 125 | "toidentifier": "1.0.0" 126 | } 127 | }, 128 | "inherits": { 129 | "version": "2.0.4", 130 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 131 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 132 | }, 133 | "mime": { 134 | "version": "1.6.0", 135 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 136 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 137 | }, 138 | "ms": { 139 | "version": "2.0.0", 140 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 141 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 142 | }, 143 | "on-finished": { 144 | "version": "2.3.0", 145 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 146 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 147 | "requires": { 148 | "ee-first": "1.1.1" 149 | } 150 | }, 151 | "parseurl": { 152 | "version": "1.3.3", 153 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 154 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 155 | }, 156 | "range-parser": { 157 | "version": "1.2.1", 158 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 159 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 160 | }, 161 | "send": { 162 | "version": "0.17.1", 163 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 164 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 165 | "requires": { 166 | "debug": "2.6.9", 167 | "depd": "~1.1.2", 168 | "destroy": "~1.0.4", 169 | "encodeurl": "~1.0.2", 170 | "escape-html": "~1.0.3", 171 | "etag": "~1.8.1", 172 | "fresh": "0.5.2", 173 | "http-errors": "~1.7.2", 174 | "mime": "1.6.0", 175 | "ms": "2.1.1", 176 | "on-finished": "~2.3.0", 177 | "range-parser": "~1.2.1", 178 | "statuses": "~1.5.0" 179 | }, 180 | "dependencies": { 181 | "ms": { 182 | "version": "2.1.1", 183 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 184 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 185 | } 186 | } 187 | }, 188 | "serve-static": { 189 | "version": "1.14.1", 190 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 191 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 192 | "requires": { 193 | "encodeurl": "~1.0.2", 194 | "escape-html": "~1.0.3", 195 | "parseurl": "~1.3.3", 196 | "send": "0.17.1" 197 | } 198 | }, 199 | "setprototypeof": { 200 | "version": "1.1.1", 201 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 202 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 203 | }, 204 | "statuses": { 205 | "version": "1.5.0", 206 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 207 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 208 | }, 209 | "supports-color": { 210 | "version": "7.1.0", 211 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 212 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 213 | "requires": { 214 | "has-flag": "^4.0.0" 215 | } 216 | }, 217 | "toidentifier": { 218 | "version": "1.0.0", 219 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 220 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 221 | }, 222 | "unpipe": { 223 | "version": "1.0.0", 224 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 225 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 226 | }, 227 | "utils-merge": { 228 | "version": "1.0.1", 229 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 230 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 231 | }, 232 | "ward-server": { 233 | "version": "1.1.1", 234 | "resolved": "https://registry.npmjs.org/ward-server/-/ward-server-1.1.1.tgz", 235 | "integrity": "sha512-gVBLVmCTiUkfndulfQuvhSN6fF1Z9IV4yoOj7bLS5s//5fCwHu4VaSaZxM+a2klTceSfyKzSinry7s3mvqun4g==", 236 | "requires": { 237 | "chalk": "^3.0.0", 238 | "connect": "^3.7.0", 239 | "serve-static": "^1.14.1" 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ward-project", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "ward-server": "^1.1.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /procfile: -------------------------------------------------------------------------------- 1 | web: node server.js --port=$PORT -------------------------------------------------------------------------------- /public/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Nunito&display=swap'); 2 | @import url('https://fonts.googleapis.com/css?family=Playfair+Display&display=swap'); 3 | 4 | viewbody { 5 | background-color: #08002e; 6 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 1200 800'%3E%3Cdefs%3E%3CradialGradient id='a' cx='0' cy='800' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23158295'/%3E%3Cstop offset='1' stop-color='%23158295' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='1200' cy='800' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23020287'/%3E%3Cstop offset='1' stop-color='%23020287' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='c' cx='600' cy='0' r='600' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%232adcd8'/%3E%3Cstop offset='1' stop-color='%232adcd8' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='d' cx='600' cy='800' r='600' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%2308002e'/%3E%3Cstop offset='1' stop-color='%2308002e' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='e' cx='0' cy='0' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%235ec96c'/%3E%3Cstop offset='1' stop-color='%235ec96c' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='f' cx='1200' cy='0' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23072dde'/%3E%3Cstop offset='1' stop-color='%23072dde' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect fill='url(%23a)' width='1200' height='800'/%3E%3Crect fill='url(%23b)' width='1200' height='800'/%3E%3Crect fill='url(%23c)' width='1200' height='800'/%3E%3Crect fill='url(%23d)' width='1200' height='800'/%3E%3Crect fill='url(%23e)' width='1200' height='800'/%3E%3Crect fill='url(%23f)' width='1200' height='800'/%3E%3C/svg%3E"); 7 | background-attachment: fixed; 8 | background-size: cover; 9 | width: 100%; 10 | height: 100vh; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | } 15 | 16 | .container { 17 | color: white; 18 | } 19 | 20 | .title { 21 | font-size: 5em; 22 | margin: 0; 23 | text-align: center; 24 | opacity: 0.5; 25 | font-family: 'Playfair Display', serif; 26 | letter-spacing: 0.1em; 27 | font-weight: normal; 28 | } 29 | 30 | .description { 31 | text-align: center; 32 | margin: 5px 0 0 0; 33 | font-family: 'Nunito', sans-serif; 34 | } 35 | 36 | .links { 37 | list-style-type: none; 38 | margin: 15px 0 0; 39 | padding: 0; 40 | text-align: center; 41 | font-family: 'Nunito', sans-serif; 42 | } 43 | 44 | .links>li { 45 | display: inline-block; 46 | } 47 | 48 | .links>li>a { 49 | display: block; 50 | padding: 10px; 51 | background: rgba(0, 0, 0, 0.3); 52 | color: white; 53 | text-decoration: none; 54 | border-radius: 5px; 55 | cursor: pointer; 56 | transition: background 300ms ease; 57 | } 58 | 59 | .links>li>a:hover { 60 | background: rgba(0, 0, 0, 0.6); 61 | } -------------------------------------------------------------------------------- /public/assets/css/app.min.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Nunito&display=swap);@import url(https://fonts.googleapis.com/css?family=Playfair+Display&display=swap);viewbody{background-color:#08002e;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 1200 800'%3E%3Cdefs%3E%3CradialGradient id='a' cx='0' cy='800' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23158295'/%3E%3Cstop offset='1' stop-color='%23158295' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='1200' cy='800' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23020287'/%3E%3Cstop offset='1' stop-color='%23020287' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='c' cx='600' cy='0' r='600' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%232adcd8'/%3E%3Cstop offset='1' stop-color='%232adcd8' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='d' cx='600' cy='800' r='600' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%2308002e'/%3E%3Cstop offset='1' stop-color='%2308002e' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='e' cx='0' cy='0' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%235ec96c'/%3E%3Cstop offset='1' stop-color='%235ec96c' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='f' cx='1200' cy='0' r='800' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23072dde'/%3E%3Cstop offset='1' stop-color='%23072dde' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect fill='url(%23a)' width='1200' height='800'/%3E%3Crect fill='url(%23b)' width='1200' height='800'/%3E%3Crect fill='url(%23c)' width='1200' height='800'/%3E%3Crect fill='url(%23d)' width='1200' height='800'/%3E%3Crect fill='url(%23e)' width='1200' height='800'/%3E%3Crect fill='url(%23f)' width='1200' height='800'/%3E%3C/svg%3E");background-attachment:fixed;background-size:cover;width:100%;height:100vh;display:flex;align-items:center;justify-content:center}.container{color:#fff}.title{font-size:5em;margin:0;text-align:center;opacity:.5;font-family:'Playfair Display',serif;letter-spacing:.1em;font-weight:400}.description{text-align:center;margin:5px 0 0 0;font-family:Nunito,sans-serif}.links{list-style-type:none;margin:15px 0 0;padding:0;text-align:center;font-family:Nunito,sans-serif}.links>li{display:inline-block}.links>li>a{display:block;padding:10px;background:rgba(0,0,0,.3);color:#fff;text-decoration:none;border-radius:5px;cursor:pointer;transition:background .3s ease}.links>li>a:hover{background:rgba(0,0,0,.6)} -------------------------------------------------------------------------------- /public/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ward-framework/ward/4c99295de16e0b155f5e7353f2a987e64d470cec/public/assets/images/logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Server = require('ward-server'); 2 | 3 | // Create a Server instance 4 | const server = new Server({ 5 | path : __dirname, 6 | port: process.env.PORT || 8000, 7 | }); 8 | 9 | // Serve files 10 | server.serve(); --------------------------------------------------------------------------------