Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c59b917159 |
@@ -1,37 +0,0 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the master branch
|
||||
push:
|
||||
branches: [ master ]
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Publish changed images
|
||||
uses: dawidd6/action-docker-publish-changed@v3
|
||||
with:
|
||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||
password: ${{secrets.DOCKERHUB_TOKEN}}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
tag: latest
|
||||
@@ -1,3 +1,26 @@
|
||||
# Include your project-specific ignores in this file
|
||||
# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
|
||||
# Useful .gitignore templates: https://github.com/github/gitignore
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
/.idea/
|
||||
/node_modules/
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="recompileOnChanges" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM nginx:latest
|
||||
COPY /. /usr/share/nginx/html
|
||||
@@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
@@ -1,34 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/)
|
||||
|
||||
## Getting started
|
||||
|
||||
* [Usage](usage.md) — Overview of the project contents.
|
||||
* [FAQ](faq.md) — Frequently asked questions along with their answers.
|
||||
|
||||
## HTML5 Boilerplate core
|
||||
|
||||
* [HTML](html.md) — Guide to the default HTML.
|
||||
* [CSS](css.md) — Guide to the default CSS.
|
||||
* [JavaScript](js.md) — Guide to the default JavaScript.
|
||||
* [Everything else](misc.md).
|
||||
|
||||
## Development
|
||||
|
||||
* [Extending and customizing HTML5 Boilerplate](extend.md) — Going further
|
||||
with the boilerplate.
|
||||
|
||||
## Related projects
|
||||
|
||||
The [H5BP organization](https://github.com/h5bp) maintains several projects
|
||||
that complement HTML5 Boilerplate, projects that can help you improve different
|
||||
aspects of your website/web app (e.g.: the performance, security, etc.).
|
||||
|
||||
* [Server Configs](https://github.com/h5bp/server-configs) — Fast and
|
||||
smart configurations for web servers such as Apache and Nginx.
|
||||
* [Apache](https://github.com/h5bp/server-configs-apache)
|
||||
* [Google App Engine (GAE)](https://github.com/h5bp/server-configs-gae)
|
||||
* [Internet Information Services (IIS)](https://github.com/h5bp/server-configs-iis)
|
||||
* [lighttpd](https://github.com/h5bp/server-configs-lighttpd)
|
||||
* [Nginx](https://github.com/h5bp/server-configs-nginx)
|
||||
* [Node.js](https://github.com/h5bp/server-configs-node)
|
||||
* [Front-end Developer Interview Questions](https://github.com/h5bp/Front-end-Developer-Interview-Questions)
|
||||
@@ -1,49 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# The CSS
|
||||
|
||||
HTML5 Boilerplate's CSS includes:
|
||||
|
||||
* [Normalize.css](#normalizecss)
|
||||
* [main.css](#maincss)
|
||||
|
||||
This starting CSS does not rely on the presence of
|
||||
[conditional class names](https://www.paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/),
|
||||
[conditional style sheets](https://css-tricks.com/how-to-create-an-ie-only-stylesheet/),
|
||||
or [Modernizr](https://modernizr.com/), and it is ready to use no matter what
|
||||
your development preferences happen to be.
|
||||
|
||||
|
||||
## Normalize.css
|
||||
|
||||
In order to make browsers render all elements more consistently and in line
|
||||
with modern standards, we include Normalize.css — a modern, HTML5-ready
|
||||
alternative to CSS resets.
|
||||
|
||||
As opposed to CSS resets, Normalize.css:
|
||||
|
||||
* targets only the styles that need normalizing
|
||||
* preserves useful browser defaults rather than erasing them
|
||||
* corrects bugs and common browser inconsistencies
|
||||
* improves usability with subtle improvements
|
||||
* doesn't clutter the debugging tools
|
||||
* has better documentation
|
||||
|
||||
For more information about Normalize.css, please refer to its [project
|
||||
page](https://necolas.github.io/normalize.css/).
|
||||
|
||||
|
||||
## main.css
|
||||
|
||||
Several base styles are included that build upon `Normalize.css`. These
|
||||
styles:
|
||||
|
||||
* provide basic typography settings that improve text readability
|
||||
* protect against unwanted `text-shadow` during text highlighting
|
||||
* tweak the default alignment of some elements (e.g.: `img`, `video`,
|
||||
`fieldset`, `textarea`)
|
||||
* style the prompt that is displayed to users using an outdated browser
|
||||
* and more...
|
||||
|
||||
These styles are included in [main.css](https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css). See the [main.css](https://github.com/h5bp/main.css) project [documentation](https://github.com/h5bp/main.css/blob/master/README.md#features) for a full discussion of these styles.
|
||||
@@ -1,639 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# Extend and customise HTML5 Boilerplate
|
||||
|
||||
Here is some useful advice for how you can make your project with HTML5
|
||||
Boilerplate even better. We don't want to include it all by default, as
|
||||
not everything fits with everyone's needs.
|
||||
|
||||
|
||||
* [App Stores](#app-stores)
|
||||
* [DNS prefetching](#dns-prefetching)
|
||||
* [Google Universal Analytics](#google-universal-analytics)
|
||||
* [Internet Explorer](#internet-explorer)
|
||||
* [Miscellaneous](#miscellaneous)
|
||||
* [News Feeds](#news-feeds)
|
||||
* [Search](#search)
|
||||
* [Social Networks](#social-networks)
|
||||
* [URLs](#urls)
|
||||
* [Web Apps](#web-apps)
|
||||
* [security.txt](#security.txt)
|
||||
|
||||
## App Stores
|
||||
|
||||
### Smart App Banners in iOS 6+ Safari
|
||||
|
||||
Stop bothering everyone with gross modals advertising your entry in the
|
||||
App Store. Including the following [meta tag](https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html) will unobtrusively give the user the option to download your iOS app, or open it with some data about the user's current state on the website.
|
||||
|
||||
```html
|
||||
<meta name="apple-itunes-app" content="app-id=APP_ID,app-argument=SOME_TEXT">
|
||||
```
|
||||
|
||||
## DNS prefetching
|
||||
|
||||
In short, DNS Prefetching is a method of informing the browser of domain names
|
||||
referenced on a site so that the client can resolve the DNS for those hosts,
|
||||
cache them, and when it comes time to use them, have a faster turn around on
|
||||
the request.
|
||||
|
||||
### Implicit prefetches
|
||||
|
||||
There is a lot of prefetching done for you automatically by the browser. When
|
||||
the browser encounters an anchor in your html that does not share the same
|
||||
domain name as the current location the browser requests, from the client OS,
|
||||
the IP address for this new domain. The client first checks its cache and
|
||||
then, lacking a cached copy, makes a request from a DNS server. These requests
|
||||
happen in the background and are not meant to block the rendering of the
|
||||
page.
|
||||
|
||||
The goal of this is that when the foreign IP address is finally needed it will
|
||||
already be in the client cache and will not block the loading of the foreign
|
||||
content. Fewer requests result in faster page load times. The perception of this
|
||||
is increased on a mobile platform where DNS latency can be greater.
|
||||
|
||||
#### Disable implicit prefetching
|
||||
|
||||
```html
|
||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||
```
|
||||
|
||||
Even with X-DNS-Prefetch-Control meta tag (or http header) browsers will still
|
||||
prefetch any explicit dns-prefetch links.
|
||||
|
||||
**_WARNING:_** THIS MAY MAKE YOUR SITE SLOWER IF YOU RELY ON RESOURCES FROM
|
||||
FOREIGN DOMAINS.
|
||||
|
||||
### Explicit prefetches
|
||||
|
||||
Typically the browser only scans the HTML for foreign domains. If you have
|
||||
resources that are outside of your HTML (a javascript request to a remote
|
||||
server or a CDN that hosts content that may not be present on every page of
|
||||
your site, for example) then you can queue up a domain name to be prefetched.
|
||||
|
||||
```html
|
||||
<link rel="dns-prefetch" href="//example.com">
|
||||
<link rel="dns-prefetch" href="https://ajax.googleapis.com">
|
||||
```
|
||||
|
||||
You can use as many of these as you need, but it's best if they are all
|
||||
immediately after the [Meta
|
||||
Charset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset)
|
||||
element (which should go right at the top of the `head`), so the browser can
|
||||
act on them ASAP.
|
||||
|
||||
#### Common Prefetch Links
|
||||
|
||||
Amazon S3:
|
||||
|
||||
```html
|
||||
<link rel="dns-prefetch" href="//s3.amazonaws.com">
|
||||
```
|
||||
|
||||
Google APIs:
|
||||
|
||||
```html
|
||||
<link rel="dns-prefetch" href="https://ajax.googleapis.com">
|
||||
```
|
||||
|
||||
Microsoft Ajax Content Delivery Network:
|
||||
|
||||
```html
|
||||
<link rel="dns-prefetch" href="//ajax.microsoft.com">
|
||||
<link rel="dns-prefetch" href="//ajax.aspnetcdn.com">
|
||||
```
|
||||
|
||||
### Further reading about DNS prefetching
|
||||
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
|
||||
* https://dev.chromium.org/developers/design-documents/dns-prefetching
|
||||
* https://blogs.msdn.microsoft.com/ie/2011/03/17/internet-explorer-9-network-performance-improvements/
|
||||
|
||||
|
||||
## Google Universal Analytics
|
||||
|
||||
### More tracking settings
|
||||
|
||||
The [optimized Google Universal Analytics
|
||||
snippet](https://mathiasbynens.be/notes/async-analytics-snippet#universal-analytics)
|
||||
included with HTML5 Boilerplate includes something like this:
|
||||
|
||||
```js
|
||||
ga('create', 'UA-XXXXX-X', 'auto'); ga('send', 'pageview');
|
||||
```
|
||||
|
||||
To customize further, see Google's [Advanced
|
||||
Setup](https://developers.google.com/analytics/devguides/collection/analyticsjs/),
|
||||
[Pageview](https://developers.google.com/analytics/devguides/collection/analyticsjs/pages),
|
||||
and [Event](https://developers.google.com/analytics/devguides/collection/analyticsjs/events) Docs.
|
||||
|
||||
### Anonymize IP addresses
|
||||
|
||||
In some countries, no personal data may be transferred outside jurisdictions
|
||||
that do not have similarly strict laws (i.e. from Germany to outside the EU).
|
||||
Thus a webmaster using the Google Universal Analytics may have to ensure that
|
||||
no personal (trackable) data is transferred to the US. You can do that with
|
||||
[the `ga('set', 'anonymizeIp', true);`
|
||||
parameter](https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced#anonymizeip)
|
||||
before sending any events/pageviews. In use it looks like this:
|
||||
|
||||
```js
|
||||
ga('create', 'UA-XXXXX-X', 'auto');
|
||||
ga('set', 'anonymizeIp', true);
|
||||
ga('send', 'pageview');
|
||||
```
|
||||
|
||||
### Track jQuery AJAX requests in Google Analytics
|
||||
|
||||
An article by @JangoSteve explains how to [track jQuery AJAX requests in Google
|
||||
Analytics](https://www.alfajango.com/blog/track-jquery-ajax-requests-in-google-analytics/).
|
||||
|
||||
Add this to `plugins.js`:
|
||||
|
||||
```js
|
||||
/*
|
||||
* Log all jQuery AJAX requests to Google Analytics
|
||||
* See: https://www.alfajango.com/blog/track-jquery-ajax-requests-in-google-analytics/
|
||||
*/
|
||||
if (typeof ga !== "undefined" && ga !== null) {
|
||||
$(document).ajaxSend(function(event, xhr, settings){
|
||||
ga('send', 'pageview', settings.url);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Track JavaScript errors in Google Analytics
|
||||
|
||||
Add this function after `ga` is defined:
|
||||
|
||||
```js
|
||||
(function(window){
|
||||
var undefined,
|
||||
link = function (href) {
|
||||
var a = window.document.createElement('a');
|
||||
a.href = href;
|
||||
return a;
|
||||
};
|
||||
window.onerror = function (message, file, line, column) {
|
||||
var host = link(file).hostname;
|
||||
ga('send', {
|
||||
'hitType': 'event',
|
||||
'eventCategory': (host == window.location.hostname || host == undefined || host == '' ? '' : 'external ') + 'error',
|
||||
'eventAction': message,
|
||||
'eventLabel': (file + ' LINE: ' + line + (column ? ' COLUMN: ' + column : '')).trim(),
|
||||
'nonInteraction': 1
|
||||
});
|
||||
};
|
||||
}(window));
|
||||
```
|
||||
|
||||
### Track page scroll
|
||||
|
||||
Add this function after `ga` is defined:
|
||||
|
||||
```js
|
||||
$(function(){
|
||||
var isDuplicateScrollEvent,
|
||||
scrollTimeStart = new Date,
|
||||
$window = $(window),
|
||||
$document = $(document),
|
||||
scrollPercent;
|
||||
|
||||
$window.scroll(function() {
|
||||
scrollPercent = Math.round(100 * ($window.height() + $window.scrollTop())/$document.height());
|
||||
if (scrollPercent > 90 && !isDuplicateScrollEvent) { //page scrolled to 90%
|
||||
isDuplicateScrollEvent = 1;
|
||||
ga('send', 'event', 'scroll',
|
||||
'Window: ' + $window.height() + 'px; Document: ' + $document.height() + 'px; Time: ' + Math.round((new Date - scrollTimeStart )/1000,1) + 's'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Internet Explorer
|
||||
|
||||
### IE Pinned Sites
|
||||
|
||||
Enabling your application for pinning will allow IE users to add it to their
|
||||
Windows Taskbar and Start Menu. This comes with a range of new tools that you
|
||||
can easily configure with the elements below. See more [documentation on IE
|
||||
Pinned Sites](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/samples/gg491731(v%3dvs.85)).
|
||||
|
||||
### Name the Pinned Site for Windows
|
||||
|
||||
Without this rule, Windows will use the page title as the name for your
|
||||
application.
|
||||
|
||||
```html
|
||||
<meta name="application-name" content="Sample Title">
|
||||
```
|
||||
|
||||
### Give your Pinned Site a tooltip
|
||||
|
||||
You know — a tooltip. A little textbox that appears when the user holds their
|
||||
mouse over your Pinned Site's icon.
|
||||
|
||||
```html
|
||||
<meta name="msapplication-tooltip" content="A description of what this site does.">
|
||||
```
|
||||
|
||||
### Set a default page for your Pinned Site
|
||||
|
||||
If the site should go to a specific URL when it is pinned (such as the
|
||||
homepage), enter it here. One idea is to send it to a special URL so you can
|
||||
track the number of pinned users, like so:
|
||||
`https://www.example.com/index.html?pinned=true`
|
||||
|
||||
```html
|
||||
<meta name="msapplication-starturl" content="https://www.example.com/index.html?pinned=true">
|
||||
```
|
||||
|
||||
### Recolor IE's controls manually for a Pinned Site
|
||||
|
||||
IE will automatically use the overall color of your Pinned Site's favicon to
|
||||
shade its browser buttons. UNLESS you give it another color here. Only use
|
||||
named colors (`red`) or hex colors (`#ff0000`).
|
||||
|
||||
```html
|
||||
<meta name="msapplication-navbutton-color" content="#ff0000">
|
||||
```
|
||||
|
||||
### Manually set the window size of a Pinned Site
|
||||
|
||||
If the site should open at a certain window size once pinned, you can specify
|
||||
the dimensions here. It only supports static pixel dimensions. 800x600
|
||||
minimum.
|
||||
|
||||
```html
|
||||
<meta name="msapplication-window" content="width=800;height=600">
|
||||
```
|
||||
|
||||
### Jump List "Tasks" for Pinned Sites
|
||||
|
||||
Add Jump List Tasks that will appear when the Pinned Site's icon gets a
|
||||
right-click. Each Task goes to the specified URL, and gets its own mini icon
|
||||
(essentially a favicon, a 16x16 .ICO). You can add as many of these as you
|
||||
need.
|
||||
|
||||
```html
|
||||
<meta name="msapplication-task" content="name=Task 1;action-uri=http://host/Page1.html;icon-uri=http://host/icon1.ico">
|
||||
<meta name="msapplication-task" content="name=Task 2;action-uri=http://microsoft.com/Page2.html;icon-uri=http://host/icon2.ico">
|
||||
```
|
||||
|
||||
### (Windows 8) High quality visuals for Pinned Sites
|
||||
|
||||
Windows 8 adds the ability for you to provide a PNG tile image and specify the
|
||||
tile's background color. [Full details on the IE
|
||||
blog](https://blogs.msdn.microsoft.com/ie/2012/06/08/high-quality-visuals-for-pinned-sites-in-windows-8/).
|
||||
|
||||
* Create a 144x144 image of your site icon, filling all of the canvas, and
|
||||
using a transparent background.
|
||||
* Save this image as a 32-bit PNG and optimize it without reducing
|
||||
colour-depth. It can be named whatever you want (e.g. `metro-tile.png`).
|
||||
* To reference the tile and its color, add the HTML `meta` elements described
|
||||
in the IE Blog post.
|
||||
|
||||
### (Windows 8) Badges for Pinned Sites
|
||||
|
||||
IE will poll an XML document for badge information to display on your app's
|
||||
tile in the Start screen. The user will be able to receive these badge updates
|
||||
even when your app isn't actively running. The badge's value can be a number,
|
||||
or one of a predefined list of glyphs.
|
||||
|
||||
* [Tutorial on IEBlog with link to badge XML schema](https://blogs.msdn.microsoft.com/ie/2012/04/03/pinned-sites-in-windows-8/)
|
||||
* [Available badge values](https://docs.microsoft.com/en-us/uwp/schemas/tiles/badgeschema/element-badge)
|
||||
|
||||
```html
|
||||
<meta name="msapplication-badge" value="frequency=NUMBER_IN_MINUTES;polling-uri=https://www.example.com/path/to/file.xml">
|
||||
```
|
||||
|
||||
### Disable link highlighting upon tap in IE10
|
||||
|
||||
Similar to [-webkit-tap-highlight-color](https://davidwalsh.name/mobile-highlight-color)
|
||||
in iOS Safari. Unlike that CSS property, this is an HTML meta element, and its
|
||||
value is boolean rather than a color. It's all or nothing.
|
||||
|
||||
```html
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
```
|
||||
|
||||
You can read about this useful element and more techniques in
|
||||
[Microsoft's documentation on adapting WebKit-oriented apps for IE10](https://blogs.windows.com/buildingapps/2012/11/15/adapting-your-webkit-optimized-site-for-internet-explorer-10/)
|
||||
|
||||
## Search
|
||||
|
||||
### Direct search spiders to your sitemap
|
||||
|
||||
After creating a [sitemap](https://www.sitemaps.org/protocol.html)
|
||||
|
||||
Submit it to search engine tool:
|
||||
* [Google](https://www.google.com/webmasters/tools/sitemap-list)
|
||||
* [Bing](https://www.bing.com/toolbox/webmaster)
|
||||
* [Yandex](https://webmaster.yandex.com/)
|
||||
* [Baidu](https://zhanzhang.baidu.com/)
|
||||
OR
|
||||
Insert the following line anywhere in your robots.txt file, specifying the path to your sitemap:
|
||||
```
|
||||
Sitemap: https://example.com/sitemap_location.xml
|
||||
```
|
||||
|
||||
### Hide pages from search engines
|
||||
|
||||
According to Heather Champ, former community manager at Flickr, you should not
|
||||
allow search engines to index your "Contact Us" or "Complaints" page if you
|
||||
value your sanity. This is an HTML-centric way of achieving that.
|
||||
|
||||
```html
|
||||
<meta name="robots" content="noindex">
|
||||
```
|
||||
|
||||
**_WARNING:_** DO NOT INCLUDE ON PAGES THAT SHOULD APPEAR IN SEARCH ENGINES.
|
||||
|
||||
### Firefox and IE Search Plugins
|
||||
|
||||
Sites with in-site search functionality should be strongly considered for a
|
||||
browser search plugin. A "search plugin" is an XML file which defines how your
|
||||
plugin behaves in the browser. [How to make a browser search
|
||||
plugin](https://www.google.com/search?ie=UTF-8&q=how+to+make+browser+search+plugin).
|
||||
|
||||
```html
|
||||
<link rel="search" title="" type="application/opensearchdescription+xml" href="">
|
||||
```
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
* Use [polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills).
|
||||
|
||||
* Use [Microformats](http://microformats.org/wiki/Main_Page) (via
|
||||
[microdata](http://microformats.org/wiki/microdata)) for optimum search
|
||||
results
|
||||
[visibility](https://webmasters.googleblog.com/2009/05/introducing-rich-snippets.html).
|
||||
|
||||
* If you're building a web app you may want [native style momentum scrolling in
|
||||
iOS 5+](https://www.johanbrook.com/writings/native-style-momentum-scrolling-to-arrive-in-ios-5/)
|
||||
using `-webkit-overflow-scrolling: touch`.
|
||||
|
||||
* If you want to disable the translation prompt in Chrome or block Google
|
||||
Translate from translating your web page, use [`<meta name="google"
|
||||
content="notranslate">`](https://support.google.com/webmasters/answer/79812).
|
||||
To disable translation for a particular section of the web page, add
|
||||
[`class="notranslate"`](https://support.google.com/translate/?hl=en#2641276).
|
||||
|
||||
* If you want to disable the automatic detection and formatting of possible
|
||||
phone numbers in Safari on iOS, use [`<meta name="format-detection"
|
||||
content="telephone=no">`](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html).
|
||||
|
||||
* Avoid development/stage websites "leaking" into SERPs (search engine results
|
||||
page) by [implementing X-Robots-tag
|
||||
headers](https://github.com/h5bp/html5-boilerplate/issues/804).
|
||||
|
||||
|
||||
## News Feeds
|
||||
|
||||
### RSS
|
||||
|
||||
Have an RSS feed? Link to it here. Want to [learn how to write an RSS feed from
|
||||
scratch](http://www.rssboard.org/rss-specification)?
|
||||
|
||||
```html
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml">
|
||||
```
|
||||
|
||||
### Atom
|
||||
|
||||
Atom is similar to RSS, and you might prefer to use it instead of or in
|
||||
addition to it. [See what Atom's all
|
||||
about](https://en.wikipedia.org/wiki/Atom_(Web_standard)).
|
||||
|
||||
```html
|
||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml">
|
||||
```
|
||||
|
||||
### Pingbacks
|
||||
|
||||
Your server may be notified when another site links to yours. The href
|
||||
attribute should contain the location of your pingback service.
|
||||
|
||||
```html
|
||||
<link rel="pingback" href="">
|
||||
```
|
||||
|
||||
* High-level explanation: https://codex.wordpress.org/Introduction_to_Blogging#Pingbacks
|
||||
* Step-by-step example case: https://www.hixie.ch/specs/pingback/pingback-1.0#TOC5
|
||||
* PHP pingback service: https://web.archive.org/web/20131211032834/http://blog.perplexedlabs.com/2009/07/15/xmlrpc-pingbacks-using-php/
|
||||
|
||||
|
||||
|
||||
## Social Networks
|
||||
|
||||
### Facebook Open Graph data
|
||||
|
||||
You can control the information that Facebook and others display when users
|
||||
share your site. Below are just the most basic data points you might need. For
|
||||
specific content types (including "website"), see [Facebook's built-in Open
|
||||
Graph content
|
||||
templates](https://developers.facebook.com/docs/sharing/opengraph/using-objects).
|
||||
Take full advantage of Facebook's support for complex data and activity by
|
||||
following the [Open Graph
|
||||
tutorial](https://developers.facebook.com/docs/sharing/webmasters/getting-started).
|
||||
|
||||
For a reference of Open Graph's markup and properties, you may check
|
||||
[Facebook's Open Graph Protocol reference](https://ogp.me). Finally,
|
||||
you can validate your markup with the [Facebook Object
|
||||
Debugger](https://developers.facebook.com/tools/debug/) (needs
|
||||
registration to Facebook).
|
||||
|
||||
```html
|
||||
<meta property="fb:app_id" content="123456789">
|
||||
<meta property="og:url" content="https://www.example.com/path/to/page.html">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="">
|
||||
<meta property="og:image" content="https://www.example.com/path/to/image.jpg">
|
||||
<meta property="og:description" content="">
|
||||
<meta property="og:site_name" content="">
|
||||
<meta property="article:author" content="">
|
||||
```
|
||||
|
||||
### Twitter Cards
|
||||
|
||||
Twitter provides a snippet specification that serves a similar purpose to Open
|
||||
Graph. In fact, Twitter will use Open Graph when Cards is not available. You
|
||||
can read more about the various snippet formats and application process in the
|
||||
[official Twitter Cards documentation](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards),
|
||||
and you can validate your markup with the [Card validator](https://cards-dev.twitter.com/validator)
|
||||
(needs registration to Twitter).
|
||||
|
||||
```html
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:site" content="@site_account">
|
||||
<meta name="twitter:creator" content="@individual_account">
|
||||
<meta name="twitter:url" content="https://www.example.com/path/to/page.html">
|
||||
<meta name="twitter:title" content="">
|
||||
<meta name="twitter:description" content="">
|
||||
<meta name="twitter:image" content="https://www.example.com/path/to/image.jpg">
|
||||
```
|
||||
|
||||
### Schema.org
|
||||
|
||||
Google also provides a snippet specification that serves a similar
|
||||
purpose to Facebook's Open Graph or Twitter Cards. This metadata is a subset
|
||||
of [schema.org's microdata vocabulary](https://schema.org/), which
|
||||
covers many other schemas that can describe the content of your pages
|
||||
to search engines. For this reason, this metadata is more generic for
|
||||
SEO, notably for Google's search-engine, although this vocabulary is
|
||||
also used by Microsoft, Pinterest and Yandex.
|
||||
|
||||
You can validate your markup with the [Structured Data Testing
|
||||
Tool](https://developers.google.com/structured-data/testing-tool/).
|
||||
Also, please note that this markup requires to add attributes to your
|
||||
top `html` tag.
|
||||
|
||||
```html
|
||||
<html class="no-js" lang="" itemscope itemtype="https://schema.org/Article">
|
||||
<head>
|
||||
|
||||
<link rel="author" href="">
|
||||
<link rel="publisher" href="">
|
||||
<meta itemprop="name" content="">
|
||||
<meta itemprop="description" content="">
|
||||
<meta itemprop="image" content="">
|
||||
```
|
||||
|
||||
## URLs
|
||||
|
||||
### Canonical URL
|
||||
|
||||
Signal to search engines and others "Use this URL for this page!" Useful when
|
||||
parameters after a `#` or `?` is used to control the display state of a page.
|
||||
`https://www.example.com/cart.html?shopping-cart-open=true` can be indexed as
|
||||
the cleaner, more accurate `https://www.example.com/cart.html`.
|
||||
|
||||
```html
|
||||
<link rel="canonical" href="">
|
||||
```
|
||||
|
||||
### Separate mobile URLs
|
||||
|
||||
If you use separate URLs for desktop and mobile users, you should consider
|
||||
helping search engine algorithms better understand the configuration on your
|
||||
web site.
|
||||
|
||||
This can be done by adding the following annotations in your HTML pages:
|
||||
|
||||
* on the desktop page, add the `link rel="alternate"` tag pointing to the
|
||||
corresponding mobile URL, e.g.:
|
||||
|
||||
`<link rel="alternate" media="only screen and (max-width: 640px)" href="https://m.example.com/page.html" >`
|
||||
|
||||
* on the mobile page, add the `link rel="canonical"` tag pointing to the
|
||||
corresponding desktop URL, e.g.:
|
||||
|
||||
`<link rel="canonical" href="https://www.example.com/page.html">`
|
||||
|
||||
For more information please see:
|
||||
|
||||
* https://developers.google.com/search/mobile-sites/mobile-seo/separate-urls
|
||||
|
||||
|
||||
## Web Apps
|
||||
|
||||
There are a couple of meta tags that provide information about a web app when
|
||||
added to the Home Screen on iOS:
|
||||
|
||||
* Adding `apple-mobile-web-app-capable` will make your web app chrome-less and
|
||||
provide the default iOS app view. You can control the color scheme of the
|
||||
default view by adding `apple-mobile-web-app-status-bar-style`.
|
||||
|
||||
```html
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
```
|
||||
|
||||
* You can use `apple-mobile-web-app-title` to add a specific sites name for the
|
||||
Home Screen icon. This works since iOS 6.
|
||||
|
||||
```html
|
||||
<meta name="apple-mobile-web-app-title" content="">
|
||||
```
|
||||
|
||||
For further information please read the [official
|
||||
documentation](https://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html)
|
||||
on Apple's site.
|
||||
|
||||
|
||||
### Apple Touch Icons
|
||||
|
||||
Apple touch icons are used as icons when a user adds your webapp to the home
|
||||
screen of aniOS devices.
|
||||
|
||||
Though the dimensions of the icon can vary between iOS devices and versions
|
||||
one `180×180px` touch icon named `icon.png` and including the following in
|
||||
the `<head>` of the page is enough:
|
||||
|
||||
```html
|
||||
<link rel="apple-touch-icon" href="icon.png">
|
||||
```
|
||||
|
||||
For a more comprehensive overview, please refer to Mathias' [article on Touch
|
||||
Icons](https://mathiasbynens.be/notes/touch-icons).
|
||||
|
||||
|
||||
### Apple Touch Startup Image
|
||||
|
||||
Apart from that it is possible to add start-up screens for web apps on iOS. This
|
||||
basically works by defining `apple-touch-startup-image` with an according link
|
||||
to the image. Since iOS devices have different screen resolutions it maybe
|
||||
necessary to add media queries to detect which image to load. Here is an
|
||||
example for an iPhone:
|
||||
|
||||
```html
|
||||
<link rel="apple-touch-startup-image" media="(max-device-width: 480px) and (-webkit-min-device-pixel-ratio: 2)" href="img/startup.png">
|
||||
```
|
||||
|
||||
|
||||
### Chrome Mobile web apps
|
||||
|
||||
Chrome Mobile has a specific meta tag for making apps [installable to the
|
||||
homescreen](https://developer.chrome.com/multidevice/android/installtohomescreen)
|
||||
which tries to be a more generic replacement to Apple's proprietary meta tag:
|
||||
|
||||
```html
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
```
|
||||
|
||||
Same applies to the touch icons:
|
||||
|
||||
```html
|
||||
<link rel="icon" sizes="192x192" href="highres-icon.png">
|
||||
```
|
||||
|
||||
### Theme Color
|
||||
|
||||
You can add the [`theme-color` meta extension](https://html.spec.whatwg.org/multipage/semantics.html#meta-theme-color)
|
||||
in the `<head>` of your pages to suggest the color that browsers and
|
||||
OSes should use if they customize the display of individual pages in
|
||||
their UIs with varying colors.
|
||||
|
||||
```html
|
||||
<meta name="theme-color" content="#ff69b4">
|
||||
```
|
||||
|
||||
The `content` attribute extension can take any valid CSS color.
|
||||
|
||||
Currently, the `theme-color` meta extension is supported by [Chrome 39+
|
||||
for Android Lollipop](https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android).
|
||||
|
||||
|
||||
## security.txt
|
||||
|
||||
When security risks in web services are discovered by users they often lack the
|
||||
channels to disclose them properly. As a result, security issues may be left unreported.
|
||||
|
||||
Security.txt defines a standard to help organizations define the process for
|
||||
users to disclose security vulnerabilities securely. Include a text
|
||||
file on your server at `.well-known/security.txt` with the relevant contact details.
|
||||
|
||||
Check [https://securitytxt.org/](https://securitytxt.org/) for more details.
|
||||
@@ -1,40 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# Frequently asked questions
|
||||
|
||||
* [Why is the Google Analytics code at the bottom? Google recommends it be
|
||||
placed in the `<head>`.](#why-is-the-google-analytics-code-at-the-bottom-google-recommends-it-be-placed-in-the-head)
|
||||
* [Do I need to upgrade my site each time a new version of HTML5 Boilerplate is
|
||||
released?](#do-i-need-to-upgrade-my-site-each-time-a-new-version-of-html5-boilerplate-is-released)
|
||||
* [Where can I get help with support
|
||||
questions?](#where-can-i-get-help-with-support-questions)
|
||||
|
||||
---
|
||||
|
||||
### Why is the Google Analytics code at the bottom? Google recommends it be placed in the `<head>`.
|
||||
|
||||
The main advantage of placing it in the `<head>` is that you will track the
|
||||
user's `pageview` even if they leave the page before it has been fully loaded.
|
||||
|
||||
Here's a handy quote from [Mathias Bynens](https://mathiasbynens.be/notes/async-analytics-snippet#comment-50) about our placement choice.
|
||||
>I should point out that it’s Google — not me — recommending to place this
|
||||
script before all other scripts in the document. The only real advantage is to
|
||||
catch a pageView call if your page fails to load completely (for example, if
|
||||
the user aborts loading, or quickly closes the page, etc.). Personally, I
|
||||
wouldn’t count that as a page view, so I actually prefer to place this script
|
||||
at the bottom, after all other scripts. This keeps all the scripts together and
|
||||
reinforces that scripts at the bottom are the right move. (Usually I
|
||||
concatenate and minify all my scripts into one .js file — the GA snippet being
|
||||
the suffix.)
|
||||
|
||||
### Do I need to upgrade my site each time a new version of HTML5 Boilerplate is released?
|
||||
|
||||
No, just as you don't normally replace the foundation of a house once it
|
||||
was built. However, there is nothing stopping you from trying to work in the
|
||||
latest changes, but you'll have to assess the costs/benefits of doing so.
|
||||
|
||||
### Where can I get help with support questions?
|
||||
|
||||
Please ask for help on
|
||||
[StackOverflow](https://stackoverflow.com/questions/tagged/html5boilerplate).
|
||||
@@ -1,205 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# The HTML
|
||||
|
||||
By default, HTML5 Boilerplate provides two `html` pages:
|
||||
|
||||
* [`index.html`](#indexhtml) - a default HTML skeleton that should form the
|
||||
basis of all pages on your website
|
||||
* `404.html` - a placeholder 404 error page
|
||||
|
||||
|
||||
## `index.html`
|
||||
|
||||
|
||||
### The `no-js` Class
|
||||
|
||||
The `no-js` class is provided in order to allow you to more easily and
|
||||
explicitly add custom styles based on whether JavaScript is disabled
|
||||
(`.no-js`) or enabled (`.js`). Using this technique also helps [avoid the
|
||||
FOUC](https://www.paulirish.com/2009/avoiding-the-fouc-v3/).
|
||||
|
||||
|
||||
## Language Attribute
|
||||
|
||||
Please consider specifying the language of your content by adding a [value](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) to the `lang`
|
||||
attribute in the `<html>` as in this example:
|
||||
|
||||
```html
|
||||
<html class="no-js" lang="en">
|
||||
```
|
||||
|
||||
### The order of the `<title>` and `<meta>` tags
|
||||
|
||||
The charset declaration (`<meta charset="utf-8">`) must be included completely
|
||||
within the [first 1024 bytes of the document](https://www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#charset)
|
||||
and should be specified as early as possible (before any content that could
|
||||
be controlled by an attacker, such as a `<title>` element) in order to avoid a
|
||||
potential [encoding-related security issue](https://code.google.com/archive/p/doctype-mirror/wikis/ArticleUtf7.wiki)
|
||||
in Internet Explorer
|
||||
|
||||
## Meta Description
|
||||
|
||||
The `description` meta tag provides a short description of the page.
|
||||
In some situations this description is used as a part of the snippet
|
||||
shown in the search results.
|
||||
|
||||
```html
|
||||
<meta name="description" content="This is a description">
|
||||
```
|
||||
|
||||
Google's [Create good meta descriptions](https://support.google.com/webmasters/answer/35624?hl=en#meta-descriptions)
|
||||
documentation has useful tips on creating an effective description.
|
||||
|
||||
## Mobile Viewport
|
||||
|
||||
There are a few different options that you can use with the [`viewport` meta
|
||||
tag](https://docs.google.com/present/view?id=dkx3qtm_22dxsrgcf4 "Viewport and
|
||||
Media Queries - The Complete Idiot's Guide"). You can find out more in [the
|
||||
MDN Web Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag).
|
||||
HTML5 Boilerplate comes with a simple setup that strikes a good balance for general use cases.
|
||||
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
If you want to take advantage of edge-to-edge displays of iPhone X/XS/XR you can do
|
||||
so with additional viewport parameters. [Check the WebKit blog](https://webkit.org/blog/7929/designing-websites-for-iphone-x/)
|
||||
for details.
|
||||
|
||||
## Web App Manifest
|
||||
|
||||
HTML5 Boilerplate includes a simple web app manifest file.
|
||||
|
||||
The web app manifest is a simple JSON file that allows you to control how your
|
||||
app appears on a device's home screen, what it looks like when it launches
|
||||
in that context and what happens when it is launched. This allows for much greater
|
||||
control over the UI of a saved site or web app on a mobile device.
|
||||
|
||||
It's linked to from the HTML as follows:
|
||||
|
||||
```html
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
```
|
||||
Our [site.webmanifest](https://github.com/h5bp/html5-boilerplate/blob/master/src/site.webmanifest) contains a very skeletal "app" definition, just to show the basic usage.
|
||||
You should fill this file out with [more information about your site or application](https://developer.mozilla.org/en-US/docs/Web/Manifest)
|
||||
|
||||
## Favicons and Touch Icon
|
||||
|
||||
The shortcut icons should be put in the root directory of your site. `favicon.ico`
|
||||
is automatically picked up by browsers if it's placed in the root. HTML5
|
||||
Boilerplate comes with a default set of icons (include favicon and one Apple
|
||||
Touch Icon) that you can use as a baseline to create your own.
|
||||
|
||||
Please refer to the more detailed description in the [Extend section](extend.md)
|
||||
of these docs.
|
||||
|
||||
## The Content Area
|
||||
|
||||
The central part of the boilerplate template is pretty much empty. This is
|
||||
intentional, in order to make the boilerplate suitable for both web page and
|
||||
web app development.
|
||||
|
||||
### Browser Upgrade Prompt
|
||||
|
||||
The main content area of the boilerplate includes a prompt to install an up to
|
||||
date browser for users of IE 9 and lower. If you intended to support IE, then you
|
||||
should edit or remove the snippet of code.
|
||||
|
||||
## Modernizr
|
||||
|
||||
HTML5 Boilerplate uses a custom build of Modernizr.
|
||||
|
||||
[Modernizr](https://modernizr.com/) is a JavaScript library which adds classes to
|
||||
the `html` element based on the results of feature test and which ensures that
|
||||
all browsers can make use of HTML5 elements (as it includes the HTML5 Shiv).
|
||||
This allows you to target parts of your CSS and JavaScript based on the
|
||||
features supported by a browser.
|
||||
|
||||
Starting with version 3 Modernizr can be customized using the [modernizr-config.json](https://github.com/h5bp/html5-boilerplate/blob/master/modernizr-config.json) and the
|
||||
[Modernizr command line utility](https://www.npmjs.com/package/modernizr-cli).
|
||||
|
||||
## What About Polyfills?
|
||||
|
||||
If you need to include [polyfills](https://remysharp.com/2010/10/08/what-is-a-polyfill)
|
||||
in your project, you must make sure those load before any other JavaScript. If you're
|
||||
using a polyfill CDN service, like [cdn.polyfill.io](https://cdn.polyfill.io/),
|
||||
just put it before the other scripts in the bottom of the page:
|
||||
|
||||
```html
|
||||
<script src="js/vendor/modernizr-3.8.0.min.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v3/polyfill.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script>window.jQuery || document.write('<script src="js/vendor/jquery-3.4.1.min.js"><\/script>')</script>
|
||||
<script src="js/plugins.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
```
|
||||
|
||||
If you like to just include the polyfills yourself, you could include them in
|
||||
`js/plugins.js`. When you have a bunch of polyfills to load in, you could
|
||||
also create a `polyfills.js` file in the `js/vendor` directory or include the files
|
||||
individually and combine them using a build tool. Always ensure that the polyfills
|
||||
are all loaded before any other JavaScript.
|
||||
|
||||
There are some misconceptions about Modernizr and polyfills. It's important
|
||||
to understand that Modernizr just handles feature checking, not polyfilling
|
||||
itself. The only thing Modernizr does regarding polyfills is that the team
|
||||
maintains [a huge list of cross Browser polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills).
|
||||
|
||||
### jQuery CDN for jQuery
|
||||
|
||||
The jQuery CDN version of the jQuery JavaScript library is referenced towards
|
||||
the bottom of the page. A local fallback of jQuery is included for rare instances
|
||||
when the CDN version might not be available, and to facilitate offline
|
||||
development.
|
||||
|
||||
The jQuery CDN version was chosen over other potential candidates
|
||||
([like Google's Hosted Libraries](https://developers.google.com/speed/libraries/))
|
||||
because it's fast ([comparable or faster than Google by some
|
||||
measures](https://www.cdnperf.com/#jsdelivr,cdnjs,google,yandex,microsoft,jquery,bootstrapcdn/https/90))
|
||||
and, (unlike Google's CDN) is available to China's hundreds of millions of internet users.
|
||||
For many years we [chose](https://github.com/h5bp/html5-boilerplate/issues/1191)
|
||||
the Google Hosted version over the jQuery CDN because it was available
|
||||
over HTTPS (the jQuery CDN was not,) and it offered a better chance of
|
||||
hitting the cache lottery owing to the popularity of the Google CDN.
|
||||
The first issue is no longer valid and the second is far outweighed by
|
||||
being able to serve jQuery to users in China.
|
||||
|
||||
While the jQuery CDN is a strong default solution your site or application may
|
||||
require a different configuration. Testing your site with services like
|
||||
[WebPageTest](https://www.webpagetest.org/) and browser tools like
|
||||
[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) will help you examine the real
|
||||
world performance of your site and can show where you can optimize your specific
|
||||
site or application.
|
||||
|
||||
### Google Universal Analytics Tracking Code
|
||||
|
||||
Finally, an optimized version of the Google Universal Analytics tracking code is
|
||||
included.
|
||||
|
||||
We use `analytics.js` rather than the newer `gtag.js` as
|
||||
[it's faster and supports tasks and plugins](https://github.com/philipwalton/analyticsjs-boilerplate/issues/19#issuecomment-333714370)
|
||||
|
||||
The beacon transport mechanism is used to send all hits [which saves HTTP requests and improves performance](https://philipwalton.com/articles/the-google-analytics-setup-i-use-on-every-site-i-build/#loading-analytics.js).
|
||||
|
||||
Google recommends that this script be placed at the top of the page.
|
||||
Factors to consider: if you place this script at the top of the page, you’ll
|
||||
be able to count users who don’t fully load the page, and you’ll incur the max
|
||||
number of simultaneous connections of the browser.
|
||||
|
||||
Further information:
|
||||
|
||||
- [Introduction to
|
||||
Analytics.js](https://developers.google.com/analytics/devguides/collection/analyticsjs/)
|
||||
- [Google Analytics Demos & Tools](https://ga-dev-tools.appspot.com/)
|
||||
|
||||
**N.B.** The Google Analytics snippet is included by default mainly
|
||||
because Google Analytics is [currently one of the most popular tracking
|
||||
solutions](https://trends.builtwith.com/analytics/Google-Analytics) out there.
|
||||
However, its usage isn't set in stone, and you SHOULD consider exploring the
|
||||
[alternatives](https://en.wikipedia.org/wiki/List_of_web_analytics_software)
|
||||
and use whatever suits your needs best.
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# The JavaScript
|
||||
|
||||
Information about the default JavaScript included in the project.
|
||||
|
||||
## main.js
|
||||
|
||||
This file can be used to contain or reference your site/app JavaScript code.
|
||||
If you're working on something more advanced you might replace this file
|
||||
entirely. That's cool.
|
||||
|
||||
## plugins.js
|
||||
|
||||
This file can be used to contain all your plugins, such as jQuery plugins and
|
||||
other 3rd party scripts for a simple site.
|
||||
|
||||
One approach is to put jQuery plugins inside of a `(function($){ ...
|
||||
})(jQuery);` closure to make sure they're in the jQuery namespace safety
|
||||
blanket. Read more about [jQuery plugin
|
||||
authoring](https://learn.jquery.com/plugins/#Getting_Started).
|
||||
|
||||
By default the `plugins.js` file contains a small script to avoid `console`
|
||||
errors in browsers that lack a `console`. The script will make sure that, if
|
||||
a console method isn't available, that method will have the value of empty
|
||||
function, thus, preventing the browser from throwing an error.
|
||||
|
||||
## vendor
|
||||
|
||||
This directory can be used to contain all 3rd party library code.
|
||||
|
||||
Minified versions of the latest jQuery and Modernizr libraries are included by
|
||||
default. You may wish to create your own [custom Modernizr
|
||||
build with the online builder](https://modernizr.com/download/) or [command
|
||||
line tool](https://modernizr.com/docs#command-line-config).
|
||||
@@ -1,173 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
* [.gitignore](#gitignore)
|
||||
* [.editorconfig](#editorconfig)
|
||||
* [Server Configuration](#server-configuration)
|
||||
* [robots.txt](#robotstxt)
|
||||
* [humans.txt](#humanstxt)
|
||||
* [browserconfig.xml](#browserconfigxml)
|
||||
|
||||
--
|
||||
|
||||
## .gitignore
|
||||
|
||||
HTML5 Boilerplate includes a basic project-level `.gitignore`. This should
|
||||
primarily be used to avoid certain project-level files and directories from
|
||||
being kept under source control. Different development-environments will
|
||||
benefit from different collections of ignores.
|
||||
|
||||
OS-specific and editor-specific files should be ignored using a "global
|
||||
ignore" that applies to all repositories on your system.
|
||||
|
||||
For example, add the following to your `~/.gitconfig`, where the `.gitignore`
|
||||
in your HOME directory contains the files and directories you'd like to
|
||||
globally ignore:
|
||||
|
||||
```gitignore
|
||||
[core]
|
||||
excludesfile = ~/.gitignore
|
||||
```
|
||||
|
||||
* More on global ignores: https://help.github.com/articles/ignoring-files/
|
||||
* Comprehensive set of ignores on GitHub: https://github.com/github/gitignore
|
||||
|
||||
|
||||
## .editorconfig
|
||||
|
||||
The `.editorconfig` file is provided in order to encourage and help you and
|
||||
your team define and maintain consistent coding styles between different
|
||||
editors and IDEs.
|
||||
|
||||
By default, `.editorconfig` includes some basic
|
||||
[properties](https://editorconfig.org/#supported-properties) that reflect the
|
||||
coding styles from the files provided by default, but you can easily change
|
||||
them to better suit your needs.
|
||||
|
||||
In order for your editor/IDE to apply the
|
||||
[properties](https://editorconfig.org/#supported-properties) from the
|
||||
`.editorconfig` file, you may need to [install a
|
||||
plugin]( https://editorconfig.org/#download).
|
||||
|
||||
__N.B.__ If you aren't using the server configurations provided by HTML5
|
||||
Boilerplate, we highly encourage you to configure your server to block
|
||||
access to `.editorconfig` files, as they can disclose sensitive information!
|
||||
|
||||
For more details, please refer to the [EditorConfig
|
||||
project](https://editorconfig.org/).
|
||||
|
||||
|
||||
## Server Configuration
|
||||
|
||||
H5BP includes a [`.htaccess`](#htaccess) file for the [Apache HTTP
|
||||
server](https://httpd.apache.org/docs/). If you are not using Apache
|
||||
as your web server, then you are encouraged to download a
|
||||
[server configuration](https://github.com/h5bp/server-configs) that
|
||||
corresponds to your web server and environment.
|
||||
|
||||
A `.htaccess` (hypertext access) file is an [Apache HTTP server
|
||||
configuration file](https://github.com/h5bp/server-configs-apache).
|
||||
The `.htaccess` file is mostly used for:
|
||||
|
||||
* Rewriting URLs
|
||||
* Controlling cache
|
||||
* Authentication
|
||||
* Server-side includes
|
||||
* Redirects
|
||||
* Gzipping
|
||||
|
||||
If you have access to the main server configuration file (usually called
|
||||
`httpd.conf`), you should add the logic from the `.htaccess` file in, for
|
||||
example, a <Directory> section in the main configuration file. This is usually
|
||||
the recommended way, as using .htaccess files slows down Apache!
|
||||
|
||||
To enable Apache modules locally, please see:
|
||||
https://github.com/h5bp/server-configs-apache#enable-apache-httpd-modules.
|
||||
|
||||
In the repo the `.htaccess` is used for:
|
||||
|
||||
* Allowing cross-origin access to web fonts
|
||||
* CORS header for images when browsers request it
|
||||
* Enable `404.html` as 404 error document
|
||||
* Making the website experience better for IE users better
|
||||
* Media UTF-8 as character encoding for `text/html` and `text/plain`
|
||||
* Enabling the rewrite URLs engine
|
||||
* Forcing or removing the `www.` at the begin of a URL
|
||||
* It blocks access to directories without a default document
|
||||
* It blocks access to files that can expose sensitive information.
|
||||
* It reduces MIME type security risks
|
||||
* It forces compressing (gzipping)
|
||||
* It tells the browser whether they should request a specific file from the
|
||||
server or whether they should grab it from the browser's cache
|
||||
|
||||
When using `.htaccess` we recommend reading all inline comments (the rules after
|
||||
a `#`) in the file once. There is a bunch of optional stuff in it.
|
||||
|
||||
If you want to know more about the `.htaccess` file check out the
|
||||
[Apache HTTP server docs](https://httpd.apache.org/docs/) or more
|
||||
specifically the [htaccess
|
||||
section](https://httpd.apache.org/docs/current/howto/htaccess.html).
|
||||
|
||||
Notice that the original repo for the `.htaccess` file is [this
|
||||
one](https://github.com/h5bp/server-configs-apache).
|
||||
|
||||
|
||||
## robots.txt
|
||||
|
||||
The `robots.txt` file is used to give instructions to web robots on what can
|
||||
be crawled from the website.
|
||||
|
||||
By default, the file provided by this project includes the next two lines:
|
||||
|
||||
* `User-agent: *` - the following rules apply to all web robots
|
||||
* `Disallow:` - everything on the website is allowed to be crawled
|
||||
|
||||
If you want to disallow certain pages you will need to specify the path in a
|
||||
`Disallow` directive (e.g.: `Disallow: /path`) or, if you want to disallow
|
||||
crawling of all content, use `Disallow: /`.
|
||||
|
||||
The `/robots.txt` file is not intended for access control, so don't try to
|
||||
use it as such. Think of it as a "No Entry" sign, rather than a locked door.
|
||||
URLs disallowed by the `robots.txt` file might still be indexed without being
|
||||
crawled, and the content from within the `robots.txt` file can be viewed by
|
||||
anyone, potentially disclosing the location of your private content! So, if
|
||||
you want to block access to private content, use proper authentication instead.
|
||||
|
||||
For more information about `robots.txt`, please see:
|
||||
|
||||
* [robotstxt.org](https://www.robotstxt.org/)
|
||||
* [How Google handles the `robots.txt` file](https://developers.google.com/webmasters/control-crawl-index/docs/robots_txt)
|
||||
|
||||
## humans.txt
|
||||
|
||||
The `humans.txt` file is used to provide information about people involved with
|
||||
the website.
|
||||
|
||||
The provided file contains three sections:
|
||||
|
||||
* `TEAM` - this is intended to list the group of people responsible for the website
|
||||
* `THANKS` - this is intended to list the group of people that have contributed
|
||||
to the website
|
||||
* `TECHNOLOGY COLOPHON` - the section lists technologies used to make the website
|
||||
|
||||
For more information about `humans.txt`, please see: http://humanstxt.org/
|
||||
|
||||
|
||||
## browserconfig.xml
|
||||
|
||||
The `browserconfig.xml` file is used to customize the tile displayed when users
|
||||
pin your site to the Windows 8.1 start screen. In there you can define custom
|
||||
tile colors, custom images or even [live tiles](https://msdn.microsoft.com/library/dn455106.aspx#CreatingLiveTiles).
|
||||
|
||||
By default, the file points to 2 placeholder tile images:
|
||||
|
||||
* `tile.png` (558x558px): used for `Small`, `Medium` and `Large` tiles.
|
||||
This image resizes automatically when necessary.
|
||||
* `tile-wide.png` (558x270px): user for `Wide` tiles.
|
||||
|
||||
Notice that IE11 uses the same images when adding a site to the `favorites`.
|
||||
|
||||
For more in-depth information about the `browserconfig.xml` file, please
|
||||
see [MSDN](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/dn320426(v=vs.85)).
|
||||
@@ -1,130 +0,0 @@
|
||||
[HTML5 Boilerplate homepage](https://html5boilerplate.com/) | [Documentation
|
||||
table of contents](TOC.md)
|
||||
|
||||
# Usage
|
||||
|
||||
The most basic usage of HTML5 Boilerplate is to create a static site or simple
|
||||
app. Once you've downloaded or cloned the project, that process looks something
|
||||
like this:
|
||||
|
||||
1. Set up the basic structure of the site.
|
||||
2. Add some content, style, and functionality.
|
||||
3. Run your site locally to see how it looks.
|
||||
4. Deploy your site.
|
||||
|
||||
Cool, right? _It is_. That said, the smart defaults, baseline elements, default
|
||||
attribute values and various other utilities that HTML5 Boilerplate offers can
|
||||
serve as the foundation for whatever you're interested in building.
|
||||
|
||||
Even the basic use-case of a simple static site can be enhanced by manipulating
|
||||
the code through an automated build process. Moving up in complexity HTML5
|
||||
Boilerplate can be integrated with whatever front-end framework, CMS or
|
||||
e-commerce platform you're working with. Mix-and-match to your heart's content.
|
||||
Use what you need (toss it in a blender if you need to) and discard the rest.
|
||||
HTML5 Boilerplate is a starting point, not a destination.
|
||||
|
||||
## Basic structure
|
||||
|
||||
A basic HTML5 Boilerplate site initially looks something like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── css
|
||||
│ ├── main.css
|
||||
│ └── normalize.css
|
||||
├── doc
|
||||
├── img
|
||||
├── js
|
||||
│ ├── main.js
|
||||
│ ├── plugins.js
|
||||
│ └── vendor
|
||||
│ ├── jquery.min.js
|
||||
│ └── modernizr.min.js
|
||||
├── .editorconfig
|
||||
├── .htaccess
|
||||
├── 404.html
|
||||
├── browserconfig.xml
|
||||
├── favicon.ico
|
||||
├── humans.txt
|
||||
├── icon.png
|
||||
├── index.html
|
||||
├── robots.txt
|
||||
├── site.webmanifest
|
||||
├── tile.png
|
||||
└── tile-wide.png
|
||||
```
|
||||
|
||||
What follows is a general overview of each major part and how to use them.
|
||||
|
||||
### css
|
||||
|
||||
This directory should contain all your project's CSS files. It includes some
|
||||
initial CSS to help get you started from a solid foundation. [About the
|
||||
CSS](css.md).
|
||||
|
||||
### doc
|
||||
|
||||
This directory contains all the HTML5 Boilerplate documentation. You can use it
|
||||
as the location and basis for your own project's documentation.
|
||||
|
||||
### js
|
||||
|
||||
This directory should contain all your project's JS files. Libraries, plugins,
|
||||
and custom code can all be included here. It includes some initial JS to help
|
||||
get you started. [About the JavaScript](js.md).
|
||||
|
||||
### .htaccess
|
||||
|
||||
The default web server configs are for Apache. For more information, please
|
||||
refer to the [Apache Server Configs
|
||||
repository](https://github.com/h5bp/server-configs-apache).
|
||||
|
||||
Host your site on a server other than Apache? You're likely to find the
|
||||
corresponding server configs project listed in our [Server Configs
|
||||
](https://github.com/h5bp/server-configs/blob/master/README.md) repository.
|
||||
|
||||
### 404.html
|
||||
|
||||
A helpful custom 404 to get you started.
|
||||
|
||||
### browserconfig.xml
|
||||
|
||||
This file contains all settings regarding custom tiles for IE11 and Edge.
|
||||
|
||||
For more info on this topic, please refer to
|
||||
[Microsoft's Docs](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/dn320426(v=vs.85)).
|
||||
|
||||
### .editorconfig
|
||||
|
||||
The `.editorconfig` file is provided in order to encourage and help you and
|
||||
your team to maintain consistent coding styles between different
|
||||
editors and IDEs. [Read more about the `.editorconfig` file](misc.md#editorconfig).
|
||||
|
||||
### index.html
|
||||
|
||||
This is the default HTML skeleton that should form the basis of all pages on
|
||||
your site. If you are using a server-side templating framework, then you will
|
||||
need to integrate this starting HTML with your setup.
|
||||
|
||||
Make sure that you update the URLs for the referenced CSS and JavaScript if you
|
||||
modify the directory structure at all.
|
||||
|
||||
If you are using Google Universal Analytics, make sure that you edit the
|
||||
corresponding snippet at the bottom to include your analytics ID.
|
||||
|
||||
### humans.txt
|
||||
|
||||
Edit this file to include the team that worked on your site/app, and the
|
||||
technology powering it.
|
||||
|
||||
### robots.txt
|
||||
|
||||
Edit this file to include any pages you need hidden from search engines.
|
||||
|
||||
### Icons
|
||||
|
||||
Replace the default `favicon.ico`, `tile.png`, `tile-wide.png` and Apple
|
||||
Touch Icon with your own.
|
||||
|
||||
If you want to use different Apple Touch Icons for different resolutions please
|
||||
refer to the [according documentation](extend.md#apple-touch-icons).
|
||||
@@ -1,510 +0,0 @@
|
||||
// Setup
|
||||
let subscriptions = [];
|
||||
const currencies = new Map();
|
||||
currencies.set("$", 6.78);
|
||||
currencies.set("€", 7.47);
|
||||
currencies.set("DKK", 1);
|
||||
// The name of the data in localStorage
|
||||
const localDataName = "subscriptionInformation";
|
||||
let localSortPreferenceName = "sortPreference";
|
||||
// Initial scroll distance to bottom of the page.
|
||||
let scrollDistance = getDocHeight();
|
||||
// Function that top/bottom button calls. Scrolls to top if user has scrolled 50% down, bottom otherwise.
|
||||
let scrollToTopOrBottom = () => {
|
||||
window.scrollTo({
|
||||
top: scrollDistance,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
// Gets the height of the page.
|
||||
function getDocHeight() {
|
||||
let D = document;
|
||||
return Math.max(D.body.scrollHeight, D.documentElement.scrollHeight, D.body.offsetHeight, D.documentElement.offsetHeight, D.body.clientHeight, D.documentElement.clientHeight);
|
||||
}
|
||||
// Keep track of the window height.
|
||||
let winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||
window.onresize = () => {
|
||||
winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||
};
|
||||
// Returns true if valid subscription, false if not.
|
||||
let isValidNewSubscription = (subscription) => {
|
||||
// Title of subscription must be unique.
|
||||
if (subscriptions.map((sub) => sub.title).includes(subscription.title)) {
|
||||
alert("Subscription name not unique");
|
||||
return false;
|
||||
}
|
||||
return isValidSubscription(subscription);
|
||||
};
|
||||
let isValidSubscription = (subscription) => {
|
||||
if (isNaN(subscription.price)) {
|
||||
alert("Amount is not a number");
|
||||
return false;
|
||||
}
|
||||
if (!([...currencies.keys()].includes(subscription.currency))) {
|
||||
alert("Currency is not a valid currency");
|
||||
return false;
|
||||
}
|
||||
if (isNaN(subscription.months)) {
|
||||
alert("Months are not a number");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
let createNewSubscription = (title, price, currency, months, notes) => {
|
||||
const newSub = {
|
||||
changed: false,
|
||||
currency: currency,
|
||||
indkk: 0,
|
||||
monthly: 0,
|
||||
months: months,
|
||||
notes: notes,
|
||||
percentOfWhole: null,
|
||||
price: price,
|
||||
title: title
|
||||
};
|
||||
newSub.monthly = newSub.price / newSub.months;
|
||||
newSub.indkk = newSub.monthly * currencies.get(newSub.currency);
|
||||
return newSub;
|
||||
};
|
||||
// Create a new subscription
|
||||
let newSubscription = () => {
|
||||
// Open the popup
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Insert subscription information',
|
||||
html: '<hr class="is-black">' +
|
||||
'<label class="label has-text-left">Title</label> ' +
|
||||
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="">' +
|
||||
'<label class="label has-text-left">Amount</label> ' +
|
||||
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="">' +
|
||||
'<label class="label has-text-left">Currency</label> ' +
|
||||
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="">' +
|
||||
'<label class="label has-text-left">Over how many months</label> ' +
|
||||
'<input id="swal-input4" class="swal2-input" placeholder="Over how many months" value="">' +
|
||||
'<label class="label has-text-left">Other notes</label> ' +
|
||||
'<textarea id="swal-input5" class="textarea" placeholder="Notes" rows="4" cols="4">' + '</textarea>',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
// Create a new temporary subscription
|
||||
const title = document.getElementById('swal-input1').value;
|
||||
const price = parseFloat(document.getElementById('swal-input2').value);
|
||||
const currency = document.getElementById('swal-input3').value;
|
||||
const months = parseInt(document.getElementById('swal-input4').value);
|
||||
const notes = document.getElementById('swal-input5').value;
|
||||
const newSub = createNewSubscription(title, price, currency, months, notes);
|
||||
// Do some form validation
|
||||
let valid = isValidNewSubscription(newSub);
|
||||
// Update the table if form was validated successfully
|
||||
if (valid) {
|
||||
subscriptions.push(newSub);
|
||||
updateTable();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
// Edit a subscription
|
||||
let editSubscription = (btn) => {
|
||||
// Find out which subscription we're editing
|
||||
let currentSubscription = getSubscriptionFromButton(btn);
|
||||
// Open the popup, and fill in the current subscription information in the form
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Insert subscription information',
|
||||
html: '<hr class="is-black">' +
|
||||
'<label class="label has-text-left">Title</label> ' +
|
||||
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="' + currentSubscription.title + '">' +
|
||||
'<label class="label has-text-left">Amount</label> ' +
|
||||
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="' + currentSubscription.price + '">' +
|
||||
'<label class="label has-text-left">Currency</label> ' +
|
||||
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="' + currentSubscription.currency + '">' +
|
||||
'<label class="label has-text-left">Over how many months</label> ' +
|
||||
'<input id="swal-input4" class="swal2-input" placeholder="Over how many months" value="' + currentSubscription.months + '">' +
|
||||
'<label class="label has-text-left">Other notes</label> ' +
|
||||
'<textarea id="swal-input5" class="textarea" placeholder="Notes" rows="4" cols="4">' + currentSubscription.notes + '</textarea>',
|
||||
focusConfirm: false,
|
||||
cancelButtonColor: '#d33',
|
||||
showCancelButton: true,
|
||||
preConfirm: () => {
|
||||
// Create a new temporary subscription
|
||||
const title = document.getElementById('swal-input1').value;
|
||||
const price = parseFloat(document.getElementById('swal-input2').value);
|
||||
const currency = document.getElementById('swal-input3').value;
|
||||
const months = parseInt(document.getElementById('swal-input4').value);
|
||||
const notes = document.getElementById('swal-input5').value;
|
||||
const editedSub = createNewSubscription(title, price, currency, months, notes);
|
||||
let valid = isValidSubscription(editedSub);
|
||||
console.log(valid);
|
||||
if (valid) {
|
||||
console.log(editedSub);
|
||||
// Update the table if form was validated successfully
|
||||
deleteSubscription(currentSubscription);
|
||||
subscriptions.push(editedSub);
|
||||
updateTable();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
// Add a new currency. Called by the add currency button
|
||||
let addCurrency = () => {
|
||||
// Fire popup
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Insert currency information',
|
||||
html: '<input id="swal-input1" class="swal2-input" placeholder="Currency Symbol">' +
|
||||
'<input id="swal-input2" class="swal2-input" placeholder="DKK Rate">',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
// Register the new possible currency
|
||||
let newCurrency = {
|
||||
symbol: document.getElementById('swal-input1').value,
|
||||
rate: parseFloat(document.getElementById('swal-input2').value),
|
||||
};
|
||||
// Check that the rate is a number
|
||||
if (isNaN(newCurrency.rate)) {
|
||||
alert("DKK exchange rate is not a number");
|
||||
return;
|
||||
}
|
||||
// Update with the new currency, and refresh the table
|
||||
currencies.set(newCurrency.symbol, newCurrency.rate);
|
||||
updateTable();
|
||||
}
|
||||
});
|
||||
};
|
||||
// Delete all subscriptions.
|
||||
let Reset = () => {
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
subscriptions.length = 0;
|
||||
updateTable();
|
||||
// @ts-ignore
|
||||
Swal.fire('Reset!', 'Your subscriptions have been reset.', 'success');
|
||||
}
|
||||
});
|
||||
};
|
||||
// Method for updating the table with new information.
|
||||
let updateTable = () => {
|
||||
// Create a new replacement tbody
|
||||
let newTbody = document.createElement('tbody');
|
||||
newTbody.classList.add("tbody");
|
||||
// Sort by users preference
|
||||
// Add rows from data in memory.
|
||||
populateTable(newTbody);
|
||||
// Replace the old tbody with the new cleared one
|
||||
let oldTbody = document.querySelector('.tbody');
|
||||
oldTbody.parentNode.replaceChild(newTbody, oldTbody);
|
||||
// Save the new information locally.
|
||||
setStored(localDataName, JSON.stringify(getSaveInfo()));
|
||||
};
|
||||
// Populate a tbody with subscriptions from the subscriptions array.
|
||||
let populateTable = (tbody) => {
|
||||
// Store the total cost of all subscriptions
|
||||
if (subscriptions.length <= 0) {
|
||||
return;
|
||||
}
|
||||
let total = subscriptions.map((s) => s.indkk).reduce((acc, sub) => acc + sub);
|
||||
// Update tablefoot with total monthly cost and amount of subscriptions.
|
||||
document.querySelector('#total').innerHTML = total.toFixed(2);
|
||||
document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length.toString();
|
||||
// Begin to create table rows
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
// Select a subscription on index i, and create a new empty row
|
||||
let elem = subscriptions[i];
|
||||
let tr = document.createElement('tr');
|
||||
// Calculate the subscriptions percentage-wise cost of the monthly total.
|
||||
elem.percentOfWhole = elem.indkk / total * 100;
|
||||
// If this element has been changed (ie. created, moved), mark it for the user.
|
||||
if (elem.changed) {
|
||||
tr.classList.add("is-selected");
|
||||
elem.changed = false;
|
||||
}
|
||||
// Create the row.
|
||||
tr.innerHTML = '<td>' + elem.title + '</td>' +
|
||||
'<td>' + elem.monthly.toFixed(2) + '</td>' +
|
||||
'<td>' + elem.currency + '</td>' +
|
||||
'<td>' + elem.indkk.toFixed(2) + '</td>' +
|
||||
'<td>' + elem.percentOfWhole.toFixed(2) + '%</td>' +
|
||||
'<td>' + '<button class="button" onclick="getInfoOfSubscription(this)">Info</button>' + '</td>' +
|
||||
'<td>' + '<button class="button" onclick="askToDeleteRow(this)">Delete</button>' + '</td>' +
|
||||
'<td>' + `<button class="button" onclick="editSubscription(this)">Edit</button>` + '</td>';
|
||||
// Append the new row
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
};
|
||||
// Create a pie-chart with the cost of each subscription
|
||||
let PieChart = () => {
|
||||
// Get data ready for CanvasJS.
|
||||
let data = [];
|
||||
for (let subscription of subscriptions) {
|
||||
console.log(subscription);
|
||||
data.push({ y: subscription.percentOfWhole, label: subscription.title });
|
||||
}
|
||||
// Create the chart with CanvasJS.
|
||||
// @ts-ignore
|
||||
let chart = new CanvasJS.Chart("chartContainer", {
|
||||
animationEnabled: true,
|
||||
title: {
|
||||
text: "Monthly Expenses"
|
||||
},
|
||||
data: [{
|
||||
type: "pie",
|
||||
startAngle: 240,
|
||||
yValueFormatString: "##0.00\"%\"",
|
||||
indexLabel: "{label} {y}",
|
||||
dataPoints: data
|
||||
}]
|
||||
});
|
||||
// Lastly, render it.
|
||||
chart.render();
|
||||
};
|
||||
// Ask the user what to sort by, and then sorts the array.
|
||||
let Sort = async () => {
|
||||
// The input options for SWAL2
|
||||
const inputOptions = new Promise((resolve) => {
|
||||
resolve({
|
||||
'Lex': 'Lexi.',
|
||||
'Price': 'Price'
|
||||
});
|
||||
});
|
||||
// Fire the popup
|
||||
// @ts-ignore
|
||||
const { value: sortBy } = await Swal.fire({
|
||||
title: 'Sort by what',
|
||||
input: 'radio',
|
||||
inputOptions: inputOptions,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return 'You need to choose something!';
|
||||
}
|
||||
}
|
||||
});
|
||||
// Sort the array, and save the sorting preference
|
||||
if (sortBy) {
|
||||
if (sortBy == "Lex") {
|
||||
setStored(localSortPreferenceName, "Lex");
|
||||
sortSubscriptionsByName();
|
||||
updateTable();
|
||||
}
|
||||
else if (sortBy == "Price") {
|
||||
setStored(localSortPreferenceName, "Price");
|
||||
sortSubscriptionsByPrice();
|
||||
updateTable();
|
||||
}
|
||||
}
|
||||
};
|
||||
// different sorters for subscriptions array
|
||||
let sortSubscriptionsByPrice = () => {
|
||||
subscriptions.sort((a, b) => {
|
||||
if (a.indkk > b.indkk) {
|
||||
return -1;
|
||||
}
|
||||
if (b.indkk > a.indkk) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
let sortSubscriptionsByName = () => {
|
||||
subscriptions.sort((a, b) => {
|
||||
if (a.title > b.title) {
|
||||
return 1;
|
||||
}
|
||||
if (b.title > a.title) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
// Gets more information on a subscription, and displays it in a popup
|
||||
let getInfoOfSubscription = (btn) => {
|
||||
let currentSub = getSubscriptionFromButton(btn);
|
||||
// @ts-ignore
|
||||
Swal.fire(currentSub.title, `${currentSub.price}${currentSub.currency} over ${currentSub.months} months, which equates to <br>
|
||||
${currentSub.monthly}${currentSub.currency} monthly, or about ${currentSub.indkk} DKK <br> ` +
|
||||
// Display notes only if there are some.
|
||||
(currentSub.notes === undefined ? "" : (`
|
||||
Notes: <br>
|
||||
<div class="control">
|
||||
<textarea class="textarea" disabled>${currentSub.notes}</textarea>
|
||||
</div>
|
||||
`)), 'question');
|
||||
};
|
||||
// Since title is unique, this finds the title of the row from which a button was pressed, and returns that subscription from the subscription array
|
||||
let getSubscriptionFromButton = (button) => {
|
||||
let tr = button.parentNode.parentNode;
|
||||
let title = tr.firstChild.textContent;
|
||||
return subscriptions.filter(s => s.title == title)[0];
|
||||
};
|
||||
let askToDeleteRow = (btn) => {
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
deleteSubscription(getSubscriptionFromButton(btn));
|
||||
// @ts-ignore
|
||||
Swal.fire('Deleted!', 'Your file has been deleted.', 'success');
|
||||
}
|
||||
});
|
||||
};
|
||||
let deleteSubscription = (subscription) => {
|
||||
const index = subscriptions.map(s => s.title).indexOf(subscription.title);
|
||||
subscriptions.splice(index, 1);
|
||||
updateTable();
|
||||
};
|
||||
// Make subscriptions and currency ready to be stored as JSON
|
||||
let getSaveInfo = () => {
|
||||
return { version: 1, subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()] };
|
||||
};
|
||||
// Parse a save object, and use it as the current info.
|
||||
let loadSaveInfo = (loadedSave, oldLoadedSave) => {
|
||||
let actualSaveToUse = loadedSave;
|
||||
if (oldLoadedSave !== null) {
|
||||
// Old data
|
||||
console.log("loading legacy data...");
|
||||
const oldData = oldLoadedSave;
|
||||
let newData = { keys: [], subscriptions: [], values: [], version: 0 };
|
||||
newData.keys = oldData.keys;
|
||||
newData.version = 1;
|
||||
newData.values = oldData.values;
|
||||
for (let sub of oldLoadedSave.subscriptions) {
|
||||
const newSub = createNewSubscription(sub.title, sub.amount, sub.currency, sub.months, sub.notes);
|
||||
newData.subscriptions.push(newSub);
|
||||
}
|
||||
actualSaveToUse = newData;
|
||||
}
|
||||
subscriptions = actualSaveToUse.subscriptions;
|
||||
currencies.clear();
|
||||
for (let i = 0; i < actualSaveToUse.keys.length; i++) {
|
||||
currencies.set(actualSaveToUse.keys[i], actualSaveToUse.values[i]);
|
||||
}
|
||||
};
|
||||
// Save a local JSON version of the current state.
|
||||
let saveToFile = () => {
|
||||
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(getSaveInfo()));
|
||||
var downloadAnchorNode = document.createElement('a');
|
||||
downloadAnchorNode.setAttribute("href", dataStr);
|
||||
downloadAnchorNode.setAttribute("download", "save" + ".json");
|
||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
};
|
||||
// Load a local JSON version of some state.
|
||||
let load = () => {
|
||||
var input, file, fr;
|
||||
if (typeof window.FileReader !== 'function') {
|
||||
alert("The file API isn't supported on this browser yet.");
|
||||
return;
|
||||
}
|
||||
input = document.getElementById('fileinput');
|
||||
if (!input) {
|
||||
alert("Um, couldn't find the fileinput element.");
|
||||
}
|
||||
else if (!input.files) {
|
||||
alert("This browser doesn't seem to support the `files` property of file inputs.");
|
||||
}
|
||||
else if (!input.files[0]) {
|
||||
alert("Please select a file before clicking 'Load'");
|
||||
}
|
||||
else {
|
||||
file = input.files[0];
|
||||
fr = new FileReader();
|
||||
fr.onload = receivedText;
|
||||
fr.readAsText(file);
|
||||
}
|
||||
function receivedText(e) {
|
||||
let lines = e.target.result;
|
||||
let loadedSave = JSON.parse(lines);
|
||||
if (loadedSave.version == null) {
|
||||
loadSaveInfo(null, loadedSave);
|
||||
}
|
||||
else {
|
||||
loadSaveInfo(loadedSave, null);
|
||||
}
|
||||
updateTable();
|
||||
}
|
||||
};
|
||||
// Retrieve the key value pair vname, vvalue
|
||||
function getStored(vname) {
|
||||
if (typeof (Storage) !== "undefined") {
|
||||
return localStorage.getItem(vname);
|
||||
}
|
||||
else {
|
||||
return getCookie(vname);
|
||||
}
|
||||
}
|
||||
// Store the key value pair vname, vvalue
|
||||
function setStored(vname, vvalue) {
|
||||
if (typeof (Storage) !== "undefined") {
|
||||
return localStorage.setItem(vname, vvalue);
|
||||
}
|
||||
else {
|
||||
return setCookie(vname, vvalue, 1000);
|
||||
}
|
||||
}
|
||||
// Fallback if browser doesn't support HTML5 localstorage
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// Try to load data from local storage. If this fails, there's probably not any data to retrieve.
|
||||
try {
|
||||
let data = JSON.parse(getStored(localDataName));
|
||||
if (data.version == null) {
|
||||
loadSaveInfo(null, data);
|
||||
}
|
||||
else {
|
||||
loadSaveInfo(data, null);
|
||||
}
|
||||
// Find out if we want to sort the array of the users data.
|
||||
let userSortPref = getStored(localSortPreferenceName);
|
||||
if (userSortPref === undefined) {
|
||||
sortSubscriptionsByName();
|
||||
}
|
||||
else {
|
||||
if (userSortPref == "Price") {
|
||||
sortSubscriptionsByPrice();
|
||||
}
|
||||
else {
|
||||
sortSubscriptionsByName();
|
||||
}
|
||||
}
|
||||
updateTable();
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Didn't find any locally stored data");
|
||||
console.log(e);
|
||||
}
|
||||
//# sourceMappingURL=main.js.map
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "monthly-expenses-tracker",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^5.11.5",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^12.19.1",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,413 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import Button from '@material-ui/core/Button/Button';
|
||||
|
||||
/*function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload, dumbass.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
<Button variant="contained" color="primary">
|
||||
Hello World
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;*/
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { createStyles, lighten, makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableContainer from '@material-ui/core/TableContainer';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TablePagination from '@material-ui/core/TablePagination';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import TableSortLabel from '@material-ui/core/TableSortLabel';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
|
||||
interface Data {
|
||||
calories: number;
|
||||
carbs: number;
|
||||
fat: number;
|
||||
name: string;
|
||||
protein: number;
|
||||
}
|
||||
|
||||
function createData(
|
||||
name: string,
|
||||
calories: number,
|
||||
fat: number,
|
||||
carbs: number,
|
||||
protein: number,
|
||||
): Data {
|
||||
return { name, calories, fat, carbs, protein };
|
||||
}
|
||||
|
||||
const rows = [
|
||||
createData('Cupcake', 305, 3.7, 67, 4.3),
|
||||
createData('Donut', 452, 25.0, 51, 4.9),
|
||||
createData('Eclair', 262, 16.0, 24, 6.0),
|
||||
createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
|
||||
createData('Gingerbread', 356, 16.0, 49, 3.9),
|
||||
createData('Honeycomb', 408, 3.2, 87, 6.5),
|
||||
createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
|
||||
createData('Jelly Bean', 375, 0.0, 94, 0.0),
|
||||
createData('KitKat', 518, 26.0, 65, 7.0),
|
||||
createData('Lollipop', 392, 0.2, 98, 0.0),
|
||||
createData('Marshmallow', 318, 0, 81, 2.0),
|
||||
createData('Nougat', 360, 19.0, 9, 37.0),
|
||||
createData('Oreo', 437, 18.0, 63, 4.0),
|
||||
];
|
||||
|
||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type Order = 'asc' | 'desc';
|
||||
|
||||
function getComparator<Key extends keyof any>(
|
||||
order: Order,
|
||||
orderBy: Key,
|
||||
): (a: { [key in Key]: number | string }, b: { [key in Key]: number | string }) => number {
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||
}
|
||||
|
||||
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
if (order !== 0) return order;
|
||||
return a[1] - b[1];
|
||||
});
|
||||
return stabilizedThis.map((el) => el[0]);
|
||||
}
|
||||
|
||||
interface HeadCell {
|
||||
disablePadding: boolean;
|
||||
id: keyof Data;
|
||||
label: string;
|
||||
numeric: boolean;
|
||||
}
|
||||
|
||||
const headCells: HeadCell[] = [
|
||||
{ id: 'name', numeric: false, disablePadding: true, label: 'Dessert (100g serving)' },
|
||||
{ id: 'calories', numeric: true, disablePadding: false, label: 'Calories' },
|
||||
{ id: 'fat', numeric: true, disablePadding: false, label: 'Fat (g)' },
|
||||
{ id: 'carbs', numeric: true, disablePadding: false, label: 'Carbs (g)' },
|
||||
{ id: 'protein', numeric: true, disablePadding: false, label: 'Protein (g)' },
|
||||
];
|
||||
|
||||
interface EnhancedTableProps {
|
||||
classes: ReturnType<typeof useStyles>;
|
||||
numSelected: number;
|
||||
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof Data) => void;
|
||||
onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
order: Order;
|
||||
orderBy: string;
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
function EnhancedTableHead(props: EnhancedTableProps) {
|
||||
const { classes, onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort } = props;
|
||||
const createSortHandler = (property: keyof Data) => (event: React.MouseEvent<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
onChange={onSelectAllClick}
|
||||
inputProps={{ 'aria-label': 'select all desserts' }}
|
||||
/>
|
||||
</TableCell>
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.numeric ? 'right' : 'left'}
|
||||
padding={headCell.disablePadding ? 'none' : 'default'}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : 'asc'}
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<span className={classes.visuallyHidden}>
|
||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||
</span>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
const useToolbarStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
highlight:
|
||||
theme.palette.type === 'light'
|
||||
? {
|
||||
color: theme.palette.secondary.main,
|
||||
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
|
||||
}
|
||||
: {
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
},
|
||||
title: {
|
||||
flex: '1 1 100%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface EnhancedTableToolbarProps {
|
||||
numSelected: number;
|
||||
}
|
||||
|
||||
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
|
||||
const classes = useToolbarStyles();
|
||||
const { numSelected } = props;
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
className={clsx(classes.root, {
|
||||
[classes.highlight]: numSelected > 0,
|
||||
})}
|
||||
>
|
||||
{numSelected > 0 ? (
|
||||
<Typography className={classes.title} color="inherit" variant="subtitle1" component="div">
|
||||
{numSelected} selected
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography className={classes.title} variant="h6" id="tableTitle" component="div">
|
||||
Nutrition
|
||||
</Typography>
|
||||
)}
|
||||
{numSelected > 0 ? (
|
||||
<Tooltip title="Delete">
|
||||
<IconButton aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Filter list">
|
||||
<IconButton aria-label="filter list" onClick={() => {createData('DIO??!?!?', 437, 18.0, 63, 4.0); alert('added row')}}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
},
|
||||
paper: {
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
table: {
|
||||
minWidth: 750,
|
||||
},
|
||||
visuallyHidden: {
|
||||
border: 0,
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
width: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export default function EnhancedTable() {
|
||||
const classes = useStyles();
|
||||
const [order, setOrder] = React.useState<Order>('asc');
|
||||
const [orderBy, setOrderBy] = React.useState<keyof Data>('calories');
|
||||
const [selected, setSelected] = React.useState<string[]>([]);
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [dense, setDense] = React.useState(false);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(5);
|
||||
|
||||
const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof Data) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
const newSelecteds = rows.map((n) => n.name);
|
||||
setSelected(newSelecteds);
|
||||
return;
|
||||
}
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const handleClick = (event: React.MouseEvent<unknown>, name: string) => {
|
||||
const selectedIndex = selected.indexOf(name);
|
||||
let newSelected: string[] = [];
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, name);
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1));
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1));
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(
|
||||
selected.slice(0, selectedIndex),
|
||||
selected.slice(selectedIndex + 1),
|
||||
);
|
||||
}
|
||||
|
||||
setSelected(newSelected);
|
||||
};
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleChangeDense = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDense(event.target.checked);
|
||||
};
|
||||
|
||||
const isSelected = (name: string) => selected.indexOf(name) !== -1;
|
||||
|
||||
const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Paper className={classes.paper}>
|
||||
<EnhancedTableToolbar numSelected={selected.length} />
|
||||
<TableContainer>
|
||||
<Table
|
||||
className={classes.table}
|
||||
aria-labelledby="tableTitle"
|
||||
size={dense ? 'small' : 'medium'}
|
||||
aria-label="enhanced table"
|
||||
>
|
||||
<EnhancedTableHead
|
||||
classes={classes}
|
||||
numSelected={selected.length}
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onSelectAllClick={handleSelectAllClick}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={rows.length}
|
||||
/>
|
||||
<TableBody>
|
||||
{stableSort(rows, getComparator(order, orderBy))
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row, index) => {
|
||||
const isItemSelected = isSelected(row.name);
|
||||
const labelId = `enhanced-table-checkbox-${index}`;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
onClick={(event) => handleClick(event, row.name)}
|
||||
role="checkbox"
|
||||
aria-checked={isItemSelected}
|
||||
tabIndex={-1}
|
||||
key={row.name}
|
||||
selected={isItemSelected}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isItemSelected}
|
||||
inputProps={{ 'aria-labelledby': labelId }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell component="th" id={labelId} scope="row" padding="none">
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.calories}</TableCell>
|
||||
<TableCell align="right">{row.fat}</TableCell>
|
||||
<TableCell align="right">{row.carbs}</TableCell>
|
||||
<TableCell align="right">{row.protein}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={rows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onChangePage={handleChangePage}
|
||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Paper>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={dense} onChange={handleChangeDense} />}
|
||||
label="Dense padding"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import EnhancedTable from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<EnhancedTable />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
|
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 766 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -25,6 +25,8 @@
|
||||
your browser</a> to improve your experience and security.</p>
|
||||
<![endif]-->
|
||||
|
||||
<button onclick="scrollToTopOrBottom()" id="myBtn" class="button">Bottom</button>
|
||||
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-8 is-offset-2">
|
||||
<table class="table is-fullwidth is-bordered is-striped">
|
||||
@@ -88,7 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@9"></script>
|
||||
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
@@ -1,51 +1,15 @@
|
||||
interface subscription{
|
||||
title: String,
|
||||
price: number,
|
||||
currency: String,
|
||||
months: number,
|
||||
notes: String,
|
||||
monthly: number,
|
||||
indkk: number,
|
||||
percentOfWhole: number,
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
interface oldSubscription{
|
||||
title: String,
|
||||
amount: number,
|
||||
currency: String,
|
||||
months: number,
|
||||
notes: String,
|
||||
monthly: number,
|
||||
indkk: number,
|
||||
percentOfWhole: number,
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
interface oldSave{
|
||||
subscriptions: oldSubscription[],
|
||||
keys: string[], //Currencies symbols
|
||||
values: number[] // Currencies values
|
||||
}
|
||||
|
||||
interface save{
|
||||
version: number,
|
||||
subscriptions: subscription[],
|
||||
keys: String[], //Currencies symbols
|
||||
values: number[] // Currencies values
|
||||
}
|
||||
|
||||
// Setup
|
||||
let subscriptions: subscription[] = [];
|
||||
subscriptions = []
|
||||
|
||||
const currencies:Map<String, number> = new Map()
|
||||
currencies = new Map()
|
||||
currencies.set("$", 6.78)
|
||||
currencies.set("€", 7.47)
|
||||
currencies.set("DKK", 1)
|
||||
|
||||
// The name of the data in localStorage
|
||||
const localDataName = "subscriptionInformation"
|
||||
let localSortPreferenceName = "sortPreference"
|
||||
localDataName = "subscriptionInformation"
|
||||
localSortPreferenceName = "sortPreference"
|
||||
localHasChangedOrder = "hasChangedOrder"
|
||||
|
||||
// Initial scroll distance to bottom of the page.
|
||||
let scrollDistance = getDocHeight()
|
||||
@@ -75,20 +39,27 @@ window.onresize = () => {
|
||||
winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||
}
|
||||
|
||||
// Returns true if valid subscription, false if not.
|
||||
let isValidNewSubscription = (subscription): Boolean => {
|
||||
// Calculate whether the button should say top or bottom
|
||||
window.onscroll = () => {
|
||||
let docHeight = getDocHeight()
|
||||
let trackLength = docHeight - winheight
|
||||
|
||||
// Title of subscription must be unique.
|
||||
if (subscriptions.map((sub) => sub.title).includes(subscription.title)) {
|
||||
alert("Subscription name not unique")
|
||||
return false
|
||||
let scrollTop = window.pageYOffset || (document.documentElement || document.body.parentNode || document.body).scrollTop
|
||||
let pctScrolled = Math.floor(scrollTop/trackLength * 100) // gets percentage scrolled (ie: 80 or NaN if tracklength == 0)
|
||||
|
||||
if (pctScrolled > 50){
|
||||
document.querySelector('#myBtn').innerHTML = "Top";
|
||||
scrollDistance = 0
|
||||
}
|
||||
else{
|
||||
document.querySelector('#myBtn').innerHTML = "Bottom"
|
||||
scrollDistance = trackLength
|
||||
}
|
||||
|
||||
return isValidSubscription(subscription);
|
||||
}
|
||||
|
||||
let isValidSubscription = (subscription: subscription): boolean => {
|
||||
if (isNaN(subscription.price)) {
|
||||
// Returns true if valid subscription, false if not.
|
||||
let isValidSubscription = (subscription) => {
|
||||
if (isNaN(subscription.amount)) {
|
||||
alert("Amount is not a number")
|
||||
return false
|
||||
}
|
||||
@@ -101,32 +72,18 @@ let isValidSubscription = (subscription: subscription): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
let createNewSubscription = (title, price, currency, months, notes): subscription => {
|
||||
const newSub: subscription = {
|
||||
changed: false,
|
||||
currency: currency,
|
||||
indkk: 0,
|
||||
monthly: 0,
|
||||
months: months,
|
||||
notes: notes,
|
||||
percentOfWhole: null,
|
||||
price: price,
|
||||
title: title
|
||||
// Title of subscription must be unique.
|
||||
if (subscriptions.map((sub) => sub.title).includes(subscription.title)) {
|
||||
alert("Subscription name not unique")
|
||||
return false
|
||||
}
|
||||
|
||||
newSub.monthly = newSub.price / newSub.months;
|
||||
newSub.indkk = newSub.monthly * currencies.get(newSub.currency);
|
||||
|
||||
return newSub;
|
||||
return true
|
||||
}
|
||||
|
||||
// Create a new subscription
|
||||
let newSubscription = () => {
|
||||
// Open the popup
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Insert subscription information',
|
||||
html:
|
||||
@@ -144,18 +101,17 @@ let newSubscription = () => {
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
// Create a new temporary subscription
|
||||
|
||||
|
||||
const title = (<HTMLInputElement>document.getElementById('swal-input1')).value;
|
||||
const price = parseFloat((<HTMLInputElement>document.getElementById('swal-input2')).value)
|
||||
const currency = (<HTMLInputElement>document.getElementById('swal-input3')).value
|
||||
const months = parseInt((<HTMLInputElement>document.getElementById('swal-input4')).value)
|
||||
const notes = (<HTMLInputElement>document.getElementById('swal-input5')).value
|
||||
|
||||
const newSub = createNewSubscription(title, price, currency, months, notes)
|
||||
let newSub = {
|
||||
title: document.getElementById('swal-input1').value,
|
||||
amount: document.getElementById('swal-input2').value,
|
||||
currency: document.getElementById('swal-input3').value,
|
||||
months: document.getElementById('swal-input4').value,
|
||||
notes: document.getElementById('swal-input5').value,
|
||||
changed: false
|
||||
}
|
||||
|
||||
// Do some form validation
|
||||
let valid = isValidNewSubscription(newSub)
|
||||
let valid = isValidSubscription(newSub)
|
||||
|
||||
// Update the table if form was validated successfully
|
||||
if (valid){
|
||||
@@ -172,7 +128,6 @@ let editSubscription = (btn) => {
|
||||
let currentSubscription = getSubscriptionFromButton(btn)
|
||||
|
||||
// Open the popup, and fill in the current subscription information in the form
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Insert subscription information',
|
||||
html:
|
||||
@@ -180,7 +135,7 @@ let editSubscription = (btn) => {
|
||||
'<label class="label has-text-left">Title</label> ' +
|
||||
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="' + currentSubscription.title + '">' +
|
||||
'<label class="label has-text-left">Amount</label> ' +
|
||||
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="' + currentSubscription.price + '">' +
|
||||
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="' + currentSubscription.amount + '">' +
|
||||
'<label class="label has-text-left">Currency</label> ' +
|
||||
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="' + currentSubscription.currency + '">' +
|
||||
'<label class="label has-text-left">Over how many months</label> ' +
|
||||
@@ -192,22 +147,20 @@ let editSubscription = (btn) => {
|
||||
showCancelButton: true,
|
||||
preConfirm: () => {
|
||||
// Create a new temporary subscription
|
||||
const title = (<HTMLInputElement>document.getElementById('swal-input1')).value;
|
||||
const price = parseFloat((<HTMLInputElement>document.getElementById('swal-input2')).value)
|
||||
const currency = (<HTMLInputElement>document.getElementById('swal-input3')).value
|
||||
const months = parseInt((<HTMLInputElement>document.getElementById('swal-input4')).value)
|
||||
const notes = (<HTMLInputElement>document.getElementById('swal-input5')).value
|
||||
|
||||
const editedSub = createNewSubscription(title, price, currency, months, notes)
|
||||
|
||||
let valid: boolean = isValidSubscription(editedSub)
|
||||
console.log(valid)
|
||||
let newSub = {
|
||||
title: document.getElementById('swal-input1').value,
|
||||
amount: document.getElementById('swal-input2').value,
|
||||
currency: document.getElementById('swal-input3').value,
|
||||
months: document.getElementById('swal-input4').value,
|
||||
notes: document.getElementById('swal-input5').value,
|
||||
changed: false
|
||||
}
|
||||
|
||||
let valid = isValidSubscription(newSub)
|
||||
if (valid){
|
||||
console.log(editedSub)
|
||||
// Update the table if form was validated successfully
|
||||
deleteSubscription(currentSubscription)
|
||||
subscriptions.push(editedSub)
|
||||
subscriptions.push(newSub)
|
||||
updateTable()
|
||||
}
|
||||
}
|
||||
@@ -217,7 +170,6 @@ let editSubscription = (btn) => {
|
||||
// Add a new currency. Called by the add currency button
|
||||
let addCurrency = () => {
|
||||
// Fire popup
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Insert currency information',
|
||||
html:
|
||||
@@ -227,13 +179,13 @@ let addCurrency = () => {
|
||||
preConfirm: () => {
|
||||
// Register the new possible currency
|
||||
let newCurrency = {
|
||||
symbol: (<HTMLInputElement>document.getElementById('swal-input1')).value,
|
||||
rate: parseFloat((<HTMLInputElement>document.getElementById('swal-input2')).value),
|
||||
symbol: document.getElementById('swal-input1').value,
|
||||
rate: document.getElementById('swal-input2').value,
|
||||
}
|
||||
|
||||
// Check that the rate is a number
|
||||
if (isNaN(newCurrency.rate)) {
|
||||
alert("DKK exchange rate is not a number")
|
||||
alert("DKK exchange rate is now a number")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -246,7 +198,6 @@ let addCurrency = () => {
|
||||
|
||||
// Delete all subscriptions.
|
||||
let Reset = () => {
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
@@ -257,9 +208,8 @@ let Reset = () => {
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
subscriptions.length = 0;
|
||||
subscriptions = []
|
||||
updateTable()
|
||||
// @ts-ignore
|
||||
Swal.fire(
|
||||
'Reset!',
|
||||
'Your subscriptions have been reset.',
|
||||
@@ -278,6 +228,7 @@ let updateTable = () => {
|
||||
|
||||
// Sort by users preference
|
||||
|
||||
|
||||
// Add rows from data in memory.
|
||||
populateTable(newTbody)
|
||||
|
||||
@@ -292,27 +243,31 @@ let updateTable = () => {
|
||||
// Populate a tbody with subscriptions from the subscriptions array.
|
||||
let populateTable = (tbody) => {
|
||||
// Store the total cost of all subscriptions
|
||||
if (subscriptions.length <= 0){
|
||||
return;
|
||||
total = 0
|
||||
|
||||
// calculate the monthly cost in dkk of each item, and add it to the running total
|
||||
for (elem of subscriptions) {
|
||||
elem.monthly = elem.amount / elem.months
|
||||
elem.indkk = elem.monthly * currencies.get(elem.currency)
|
||||
total += elem.indkk
|
||||
}
|
||||
let total = subscriptions.map((s) => s.indkk).reduce((acc, sub) => acc + sub)
|
||||
|
||||
// Update tablefoot with total monthly cost and amount of subscriptions.
|
||||
document.querySelector('#total').innerHTML = total.toFixed(2);
|
||||
document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length.toString();
|
||||
document.querySelector('#total').innerHTML = total.toFixed(2)
|
||||
document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length
|
||||
|
||||
// Begin to create table rows
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
// Select a subscription on index i, and create a new empty row
|
||||
let elem = subscriptions[i]
|
||||
let tr = document.createElement('tr');
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
// Calculate the subscriptions percentage-wise cost of the monthly total.
|
||||
elem.percentOfWhole = elem.indkk / total * 100;
|
||||
|
||||
// If this element has been changed (ie. created, moved), mark it for the user.
|
||||
if (elem.changed) {
|
||||
tr.classList.add("is-selected");
|
||||
tr.classList = "is-selected"
|
||||
elem.changed = false
|
||||
}
|
||||
|
||||
@@ -324,7 +279,7 @@ let populateTable = (tbody) => {
|
||||
'<td>' + elem.percentOfWhole.toFixed(2) + '%</td>' +
|
||||
'<td>' + '<button class="button" onclick="getInfoOfSubscription(this)">Info</button>' + '</td>' +
|
||||
'<td>' + '<button class="button" onclick="askToDeleteRow(this)">Delete</button>' + '</td>' +
|
||||
'<td>' + `<button class="button" onclick="editSubscription(this)">Edit</button>` + '</td>';
|
||||
'<td>' + `<button class="button" onclick="moveRow(this, -1)">Up</button>` + `<button class="button" onclick="moveRow(this, +1)">Down</button>` + `<button class="button" onclick="editSubscription(this)">Edit</button>` + '</td>';
|
||||
|
||||
// Append the new row
|
||||
tbody.appendChild(tr);
|
||||
@@ -333,15 +288,15 @@ let populateTable = (tbody) => {
|
||||
|
||||
// Create a pie-chart with the cost of each subscription
|
||||
let PieChart = () => {
|
||||
|
||||
// Get data ready for CanvasJS.
|
||||
let data = []
|
||||
for (let subscription of subscriptions) {
|
||||
for (subscription of subscriptions) {
|
||||
console.log(subscription)
|
||||
data.push({y: subscription.percentOfWhole, label: subscription.title})
|
||||
}
|
||||
|
||||
// Create the chart with CanvasJS.
|
||||
// @ts-ignore
|
||||
let chart = new CanvasJS.Chart("chartContainer", {
|
||||
animationEnabled: true,
|
||||
title: {
|
||||
@@ -372,7 +327,6 @@ let Sort = async () => {
|
||||
})
|
||||
|
||||
// Fire the popup
|
||||
// @ts-ignore
|
||||
const {value: sortBy} = await Swal.fire({
|
||||
title: 'Sort by what',
|
||||
input: 'radio',
|
||||
@@ -426,10 +380,9 @@ let sortSubscriptionsByName = () => {
|
||||
// Gets more information on a subscription, and displays it in a popup
|
||||
let getInfoOfSubscription = (btn) => {
|
||||
let currentSub = getSubscriptionFromButton(btn)
|
||||
// @ts-ignore
|
||||
Swal.fire(
|
||||
currentSub.title,
|
||||
`${currentSub.price}${currentSub.currency} over ${currentSub.months} months, which equates to <br>
|
||||
`${currentSub.amount}${currentSub.currency} over ${currentSub.months} months, which equates to <br>
|
||||
${currentSub.monthly}${currentSub.currency} monthly, or about ${currentSub.indkk} DKK <br> ` +
|
||||
// Display notes only if there are some.
|
||||
(currentSub.notes === undefined ? "" : (
|
||||
@@ -450,8 +403,34 @@ let getSubscriptionFromButton = (button) => {
|
||||
return subscriptions.filter(s => s.title == title)[0]
|
||||
}
|
||||
|
||||
// Moves a row by some direction (+x, -x)
|
||||
let moveRow = (btn, direction) => {
|
||||
|
||||
// Find the subscriptions current index
|
||||
let currentSub = getSubscriptionFromButton(btn)
|
||||
let indexOfCurrentRow = subscriptions.indexOf(currentSub)
|
||||
|
||||
// If trying to move a subscription out of bounds
|
||||
if (indexOfCurrentRow + direction < 0 || indexOfCurrentRow + direction >= subscriptions.length) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: 'Cant move this object ' + (direction == -1 ? "up" : "down"),
|
||||
})
|
||||
} else {
|
||||
// Move the subscription
|
||||
subscriptions[indexOfCurrentRow] = subscriptions[indexOfCurrentRow + direction]
|
||||
subscriptions[indexOfCurrentRow + direction] = currentSub
|
||||
currentSub.changed = true
|
||||
updateTable()
|
||||
|
||||
// Store that the user has a custom order
|
||||
setStored(localHasChangedOrder, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let askToDeleteRow = (btn) => {
|
||||
// @ts-ignore
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
@@ -463,7 +442,6 @@ let askToDeleteRow = (btn) => {
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
deleteSubscription(getSubscriptionFromButton(btn))
|
||||
// @ts-ignore
|
||||
Swal.fire(
|
||||
'Deleted!',
|
||||
'Your file has been deleted.',
|
||||
@@ -474,47 +452,22 @@ let askToDeleteRow = (btn) => {
|
||||
}
|
||||
|
||||
let deleteSubscription = (subscription) => {
|
||||
const index = subscriptions.map(s => s.title).indexOf(subscription.title);
|
||||
subscriptions.splice(index, 1);
|
||||
subscriptions = subscriptions.filter(s => s.title != subscription.title)
|
||||
updateTable()
|
||||
}
|
||||
|
||||
// Make subscriptions and currency ready to be stored as JSON
|
||||
let getSaveInfo = (): save => {
|
||||
return {version: 1, subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()]}
|
||||
let getSaveInfo = () => {
|
||||
return {subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()]}
|
||||
}
|
||||
|
||||
// Parse a save object, and use it as the current info.
|
||||
let loadSaveInfo = (loadedSave: save, oldLoadedSave: oldSave) => {
|
||||
|
||||
let actualSaveToUse = loadedSave;
|
||||
|
||||
if (oldLoadedSave !== null){
|
||||
// Old data
|
||||
|
||||
console.log("loading legacy data...")
|
||||
|
||||
const oldData: oldSave = oldLoadedSave;
|
||||
|
||||
let newData: save = {keys: [], subscriptions: [], values: [], version: 0};
|
||||
newData.keys = oldData.keys;
|
||||
newData.version = 1;
|
||||
newData.values = oldData.values;
|
||||
|
||||
for (let sub of oldLoadedSave.subscriptions){
|
||||
const newSub: subscription = createNewSubscription(sub.title, sub.amount, sub.currency, sub.months, sub.notes);
|
||||
newData.subscriptions.push(newSub);
|
||||
}
|
||||
|
||||
actualSaveToUse = newData;
|
||||
}
|
||||
|
||||
|
||||
subscriptions = actualSaveToUse.subscriptions
|
||||
let loadSaveInfo = (loadedSave) => {
|
||||
subscriptions = loadedSave.subscriptions
|
||||
|
||||
currencies.clear()
|
||||
for (let i = 0; i < actualSaveToUse.keys.length; i++) {
|
||||
currencies.set(actualSaveToUse.keys[i], actualSaveToUse.values[i])
|
||||
for (let i = 0; i < loadedSave.keys.length; i++) {
|
||||
currencies.set(loadedSave.keys[i], loadedSave.values[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,15 +507,8 @@ let load = () => {
|
||||
|
||||
function receivedText(e) {
|
||||
let lines = e.target.result;
|
||||
let loadedSave = JSON.parse(lines);
|
||||
|
||||
if (loadedSave.version == null){
|
||||
loadSaveInfo(null, loadedSave)
|
||||
}
|
||||
else{
|
||||
loadSaveInfo(loadedSave, null)
|
||||
|
||||
}
|
||||
loadedSave = JSON.parse(lines);
|
||||
loadSaveInfo(loadedSave)
|
||||
|
||||
updateTable()
|
||||
}
|
||||
@@ -582,7 +528,7 @@ function setStored(vname, vvalue) {
|
||||
if (typeof (Storage) !== "undefined") {
|
||||
return localStorage.setItem(vname, vvalue)
|
||||
} else {
|
||||
return setCookie(vname, vvalue, 1000)
|
||||
return setCookie(vname, vvalue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,16 +560,10 @@ function getCookie(cname) {
|
||||
|
||||
// Try to load data from local storage. If this fails, there's probably not any data to retrieve.
|
||||
try {
|
||||
let data = JSON.parse(getStored(localDataName));
|
||||
|
||||
if (data.version == null){
|
||||
loadSaveInfo(null, data);
|
||||
}
|
||||
else{
|
||||
loadSaveInfo(data, null)
|
||||
}
|
||||
loadSaveInfo(JSON.parse(getStored(localDataName)))
|
||||
|
||||
// Find out if we want to sort the array of the users data.
|
||||
if (!getStored(localHasChangedOrder)){
|
||||
let userSortPref = getStored(localSortPreferenceName)
|
||||
if (userSortPref === undefined){
|
||||
sortSubscriptionsByName()
|
||||
@@ -636,6 +576,7 @@ try {
|
||||
sortSubscriptionsByName()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTable()
|
||||
} catch (e) {
|
||||
console.log("Didn't find any locally stored data")
|
||||
@@ -1,5 +1,5 @@
|
||||
# www.robotstxt.org/
|
||||
# This site is based on user based content :)
|
||||
|
||||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -1,10 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"sourceMap": true
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
|
||||