├── .gitignore ├── .prettierignore ├── Procfile ├── README.md ├── app.js ├── bower_components ├── dom-to-image │ ├── .bower.json │ ├── Gruntfile.js │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── karma.conf.js │ ├── package.json │ └── test-lib │ │ └── tesseract-1.0.10.js └── file-saver │ ├── .bower.json │ ├── LICENSE.md │ ├── bower.json │ └── dist │ ├── FileSaver.js │ └── FileSaver.min.js.map ├── config.js ├── package-lock.json ├── package.json ├── public ├── FileSaver.min.js ├── Merchant Copy Doublesize.ttf ├── Merchant_Copy.ttf ├── about.html ├── ads.js ├── ads.txt ├── applemusic.html ├── applemusic.js ├── assets │ └── img │ │ ├── ReceiptifyFinal.png │ │ ├── Spotify_Logo_RGB_Black.png │ │ ├── Spotify_Logo_RGB_Green.png │ │ ├── logo.svg │ │ ├── mixes-promo-mobile.jpg │ │ ├── mixes-promo.jpg │ │ ├── store-icons.svg │ │ ├── swsh_banner.png │ │ └── swsh_banner_mobile.png ├── barcode.png ├── bitMatrix-A2.ttf ├── contact.html ├── dom-to-image.min.js ├── index.html ├── lastfm.html ├── lastfm.js ├── privacy.html ├── server.js ├── styles.css ├── tcf.js └── wrinkled-paper-texture-7.jpg └── views ├── applemusic.handlebars ├── footer.handlebars ├── header.handlebars ├── home.handlebars ├── lastfm.handlebars ├── spotify.handlebars └── spotifyReceipt.handlebars /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | public/config.js 4 | AuthKey_A8FKGGUQP3.p8 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.ejs -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Receiptify 2 | 3 | Web application inspired by https://www.instagram.com/albumreceipts/. Generates receipts that list out a user's top tracks in the past month, 6 months, and all time. 4 | 5 | The application can be viewed at https://receiptify.herokuapp.com/. 6 | 7 | NOTE: This code is admittedly not super clean as I was in quite a time crunch when I originally wrote it and never got the chance to really go back and fix everything, so sorry in advance! Despite this, I am making it public as a few people have asked me about it :) When I have time, I hope to refactor & clean this up though! 8 | 9 | ## Running the App Locally 10 | 11 | This app runs on Node.js. On [its website](http://www.nodejs.org/download/) you can find instructions on how to install it. You can also follow [this gist](https://gist.github.com/isaacs/579814) for a quick and easy way to install Node.js and npm. 12 | 13 | Once installed, clone the repository and install its dependencies running: 14 | 15 | $ npm install 16 | 17 | ### Using your own credentials 18 | 19 | You will need to register your app and get your own credentials from the Spotify for Developers Dashboard. 20 | 21 | To do so, go to [your Spotify for Developers Dashboard](https://beta.developer.spotify.com/dashboard) and create your application. In my own development process, I registered these Redirect URIs: 22 | 23 | - http://localhost:3000 (needed for the implicit grant flow) 24 | - http://localhost:3000/callback 25 | 26 | Once you have created your app, load the `client_id`, `redirect_uri` and `client_secret` into a `config.js` file. 27 | 28 | In order to run the app, open the folder, and run its `app.js` file: 29 | 30 | $ cd authorization_code 31 | $ node app.js 32 | 33 | Then, open `http://localhost:3000` in a browser. 34 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example of a basic node.js script that performs 3 | * the Authorization Code oAuth2 flow to authenticate against 4 | * the Spotify Accounts. 5 | * 6 | * For more information, read 7 | * https://developer.spotify.com/web-api/authorization-guide/#authorization_code_flow 8 | */ 9 | const express = require('express'); // Express web server framework 10 | const request = require('request'); 11 | // const axios = require("axios"); // "Request" library 12 | // const bodyParser = require("body-parser"); 13 | // const cors = require("cors"); 14 | const querystring = require('querystring'); 15 | const cookieParser = require('cookie-parser'); 16 | const fs = require('fs'); 17 | const jwt = require('jsonwebtoken'); 18 | // const https = require("https"); 19 | // const exphbs = require("express-handlebars"); 20 | const cors = require('cors'); 21 | // const { config } = require("./config"); 22 | require('dotenv').config(); 23 | 24 | const client_id = process.env.clientID; // Your client id 25 | const client_secret = process.env.clientSecret; // Your secret 26 | const privateKey = fs.readFileSync('AuthKey_A8FKGGUQP3.p8').toString(); 27 | const teamId = process.env.teamId; 28 | const keyId = process.env.keyId; 29 | 30 | var redirect_uri = process.env.redirect_uri || 'http://localhost:3000/callback'; // Your redirect uri 31 | // var redirect_uri = "http://localhost:3000/callback"; 32 | /** 33 | * Generates a random string containing numbers and letters 34 | * @param {number} length The length of the string 35 | * @return {string} The generated stringh 36 | */ 37 | var generateRandomString = function (length) { 38 | var text = ''; 39 | var possible = 40 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 41 | 42 | for (var i = 0; i < length; i++) { 43 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 44 | } 45 | return text; 46 | }; 47 | 48 | var stateKey = 'spotify_auth_state'; 49 | 50 | var app = express(); 51 | // app.engine("handlebars", exphbs({ defaultLayout: null })); 52 | // app.set("view engine", "handlebars"); 53 | // app.set("views", __dirname + "/views"); 54 | app 55 | .use(express.static(__dirname + '/public')) 56 | .use(cors()) 57 | .use(cookieParser()); 58 | 59 | app.get('/login', function (req, res) { 60 | var state = generateRandomString(16); 61 | res.cookie(stateKey, state); 62 | 63 | // your application requests authorization 64 | // user-read-private & user-read-email used to get current user info 65 | // user-top-read used to get top track info 66 | var scope = 67 | 'user-read-private user-read-email user-top-read playlist-modify-public'; 68 | res.redirect( 69 | 'https://accounts.spotify.com/authorize?' + 70 | querystring.stringify({ 71 | response_type: 'code', 72 | client_id: client_id, 73 | scope: scope, 74 | redirect_uri: redirect_uri, 75 | state: state, 76 | }) 77 | ); 78 | }); 79 | 80 | app.get('/applemusic', function (req, res) { 81 | const token = jwt.sign({}, privateKey, { 82 | algorithm: 'ES256', 83 | expiresIn: '180d', 84 | issuer: teamId, 85 | header: { 86 | alg: 'ES256', 87 | kid: keyId, 88 | }, 89 | }); 90 | 91 | res.redirect( 92 | '/#' + 93 | querystring.stringify({ 94 | client: 'applemusic', 95 | dev_token: token, 96 | }) 97 | ); 98 | // res.redirect( 99 | // 'https://idmsa.apple.com/IDMSWebAuth/auth?' + querystring.stringify({}) 100 | // ); 101 | // let music = MusicKit.getInstance(); 102 | // music.authorize().then(console.log('hello')); 103 | // res.sendFile(__dirname + '/public/applemusic.html'); 104 | }); 105 | 106 | app.get('/lastfm', function (req, res) { 107 | // res.redirect( 108 | // "/#" + 109 | // querystring.stringify({ 110 | // lastfmKey: lastfmKey, 111 | // service: "lastfm" 112 | // }) 113 | // ); 114 | res.sendFile(__dirname + '/public/lastfm.html'); 115 | }); 116 | 117 | app.get('/callback', function (req, res) { 118 | // your application requests refresh and access tokens 119 | // after checking the state parameter 120 | 121 | var code = req.query.code || null; 122 | var state = req.query.state || null; 123 | var storedState = req.cookies ? req.cookies[stateKey] : null; 124 | 125 | if (state === null || state !== storedState) { 126 | res.redirect( 127 | '/#' + 128 | querystring.stringify({ 129 | error: 'state_mismatch', 130 | }) 131 | ); 132 | } else { 133 | res.clearCookie(stateKey); 134 | var authOptions = { 135 | url: 'https://accounts.spotify.com/api/token', 136 | form: { 137 | code: code, 138 | redirect_uri: redirect_uri, 139 | grant_type: 'authorization_code', 140 | }, 141 | headers: { 142 | Authorization: 143 | 'Basic ' + 144 | new Buffer(client_id + ':' + client_secret).toString('base64'), 145 | }, 146 | json: true, 147 | }; 148 | 149 | request.post(authOptions, function (error, response, body) { 150 | if (!error && response.statusCode === 200) { 151 | access_token = body.access_token; 152 | var access_token = body.access_token, 153 | refresh_token = body.refresh_token; 154 | 155 | res.redirect( 156 | '/#' + 157 | querystring.stringify({ 158 | client: 'spotify', 159 | access_token: access_token, 160 | refresh_token: refresh_token, 161 | }) 162 | ); 163 | // res.redirect("/spotify"); 164 | // console.log(retrieveTracksSpotify(access_token, "short_term", 1, "LAST MONTH")); 165 | // res.render("spotify", { 166 | // shortTerm: retrieveTracksSpotify(access_token, "short_term", 1, "LAST MONTH"), 167 | // mediumTerm: retrieveTracksSpotify(access_token, "medium_term", 2, "LAST 6 MONTHS"), 168 | // longTerm: retrieveTracksSpotify(access_token, "long_term", 3, "ALL TIME") 169 | // }); 170 | } else { 171 | res.send('There was an error during authentication.'); 172 | } 173 | }); 174 | } 175 | }); 176 | 177 | app.get('/refresh_token', function (req, res) { 178 | // requesting access token from refresh token 179 | var refresh_token = req.query.refresh_token; 180 | var authOptions = { 181 | url: 'https://accounts.spotify.com/api/token', 182 | headers: { 183 | Authorization: 184 | 'Basic ' + 185 | new Buffer(client_id + ':' + client_secret).toString('base64'), 186 | }, 187 | form: { 188 | grant_type: 'refresh_token', 189 | refresh_token: refresh_token, 190 | }, 191 | json: true, 192 | }; 193 | 194 | request.post(authOptions, function (error, response, body) { 195 | if (!error && response.statusCode === 200) { 196 | var access_token = body.access_token; 197 | res.send({ 198 | access_token: access_token, 199 | }); 200 | } 201 | }); 202 | }); 203 | 204 | app.listen(process.env.PORT || 3000, function () { 205 | console.log('Server is running on port 3000'); 206 | }); 207 | -------------------------------------------------------------------------------- /bower_components/dom-to-image/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-to-image", 3 | "version": "2.6.0", 4 | "homepage": "https://github.com/tsayen/dom-to-image", 5 | "authors": [ 6 | "Anatolii Saienko " 7 | ], 8 | "description": "Generates an image from a DOM node using HTML5 canvas and SVG", 9 | "main": "src/dom-to-image.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "dom", 15 | "image", 16 | "raster", 17 | "render", 18 | "html", 19 | "canvas", 20 | "svg" 21 | ], 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components", 27 | "spec" 28 | ], 29 | "devDependencies": { 30 | "js-imagediff": "~1.0.8", 31 | "jquery": "~2.1.3", 32 | "fontawesome": "~4.4.0" 33 | }, 34 | "_release": "2.6.0", 35 | "_resolution": { 36 | "type": "version", 37 | "tag": "2.6.0", 38 | "commit": "ab13dce007418ab880e8b2b8a1db89b9a8087525" 39 | }, 40 | "_source": "https://github.com/tsayen/dom-to-image.git", 41 | "_target": "^2.6.0", 42 | "_originalSource": "dom-to-image", 43 | "_direct": true 44 | } -------------------------------------------------------------------------------- /bower_components/dom-to-image/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | jshint: { 6 | files: ['Gruntfile.js', 'src/**/*.js', 'spec/**/*.js'], 7 | options: { 8 | jshintrc: true 9 | } 10 | }, 11 | karma: { 12 | unit: { 13 | configFile: 'karma.conf.js', 14 | background: false, 15 | singleRun: true 16 | } 17 | }, 18 | uglify: { 19 | options: { 20 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' 21 | }, 22 | dist: { 23 | files: { 24 | 'dist/<%= pkg.name %>.min.js': ['src/dom-to-image.js'] 25 | } 26 | } 27 | }, 28 | watch: { 29 | files: ['<%= jshint.files %>'], 30 | tasks: ['test'] 31 | } 32 | }); 33 | 34 | grunt.loadNpmTasks('grunt-contrib-jshint'); 35 | grunt.loadNpmTasks('grunt-karma'); 36 | grunt.loadNpmTasks('grunt-contrib-uglify'); 37 | grunt.loadNpmTasks('grunt-contrib-watch'); 38 | 39 | grunt.registerTask('test', ['karma']); 40 | grunt.registerTask('default', ['jshint', 'test', 'uglify']); 41 | }; 42 | -------------------------------------------------------------------------------- /bower_components/dom-to-image/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Anatolii Saienko 4 | https://github.com/tsayen 5 | 6 | Copyright 2012 Paul Bakaus 7 | http://paulbakaus.com/ 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /bower_components/dom-to-image/README.md: -------------------------------------------------------------------------------- 1 | # DOM to Image 2 | 3 | [![Build Status](https://travis-ci.org/tsayen/dom-to-image.svg?branch=master)](https://travis-ci.org/tsayen/dom-to-image) 4 | 5 | ## What is it 6 | 7 | **dom-to-image** is a library which can turn arbitrary DOM node into 8 | a vector (SVG) or raster (PNG or JPEG) image, written in JavaScript. It's 9 | based on [domvas by Paul Bakaus](https://github.com/pbakaus/domvas) 10 | and has been completely rewritten, with some bugs fixed and some new 11 | features (like web font and image support) added. 12 | 13 | ## Installation 14 | 15 | ### NPM 16 | 17 | `npm install dom-to-image` 18 | 19 | Then load 20 | 21 | ```javascript 22 | /* in ES 6 */ 23 | import domtoimage from 'dom-to-image'; 24 | /* in ES 5 */ 25 | var domtoimage = require('dom-to-image'); 26 | ``` 27 | 28 | ### Bower 29 | 30 | `bower install dom-to-image` 31 | 32 | Include either `src/dom-to-image.js` or `dist/dom-to-image.min.js` in your page 33 | and it will make the `domtoimage` variable available in the global scope. 34 | 35 | ```html 36 | 41 | ``` 42 | 43 | ## Usage 44 | 45 | All the top level functions accept DOM node and rendering options, 46 | and return promises, which are fulfilled with corresponding data URLs. 47 | Get a PNG image base64-encoded data URL and display right away: 48 | 49 | ```javascript 50 | var node = document.getElementById('my-node'); 51 | 52 | domtoimage.toPng(node) 53 | .then(function (dataUrl) { 54 | var img = new Image(); 55 | img.src = dataUrl; 56 | document.body.appendChild(img); 57 | }) 58 | .catch(function (error) { 59 | console.error('oops, something went wrong!', error); 60 | }); 61 | ``` 62 | 63 | Get a PNG image blob and download it (using [FileSaver](https://github.com/eligrey/FileSaver.js/), 64 | for example): 65 | 66 | ```javascript 67 | domtoimage.toBlob(document.getElementById('my-node')) 68 | .then(function (blob) { 69 | window.saveAs(blob, 'my-node.png'); 70 | }); 71 | ``` 72 | 73 | Save and download a compressed JPEG image: 74 | 75 | ```javascript 76 | domtoimage.toJpeg(document.getElementById('my-node'), { quality: 0.95 }) 77 | .then(function (dataUrl) { 78 | var link = document.createElement('a'); 79 | link.download = 'my-image-name.jpeg'; 80 | link.href = dataUrl; 81 | link.click(); 82 | }); 83 | ``` 84 | 85 | Get an SVG data URL, but filter out all the `` elements: 86 | 87 | ```javascript 88 | function filter (node) { 89 | return (node.tagName !== 'i'); 90 | } 91 | 92 | domtoimage.toSvg(document.getElementById('my-node'), {filter: filter}) 93 | .then(function (dataUrl) { 94 | /* do something */ 95 | }); 96 | ``` 97 | 98 | Get the raw pixel data as a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) 99 | with every 4 array elements representing the RGBA data of a pixel: 100 | 101 | ```javascript 102 | var node = document.getElementById('my-node'); 103 | 104 | domtoimage.toPixelData(node) 105 | .then(function (pixels) { 106 | for (var y = 0; y < node.scrollHeight; ++y) { 107 | for (var x = 0; x < node.scrollWidth; ++x) { 108 | pixelAtXYOffset = (4 * y * node.scrollHeight) + (4 * x); 109 | /* pixelAtXY is a Uint8Array[4] containing RGBA values of the pixel at (x, y) in the range 0..255 */ 110 | pixelAtXY = pixels.slice(pixelAtXYOffset, pixelAtXYOffset + 4); 111 | } 112 | } 113 | }); 114 | ``` 115 | 116 | * * * 117 | 118 | _All the functions under `impl` are not public API and are exposed only 119 | for unit testing._ 120 | 121 | * * * 122 | 123 | ### Rendering options 124 | 125 | #### filter 126 | 127 | A function taking DOM node as argument. Should return true if passed node 128 | should be included in the output (excluding node means excluding it's 129 | children as well). Not called on the root node. 130 | 131 | #### bgcolor 132 | 133 | A string value for the background color, any valid CSS color value. 134 | 135 | #### height, width 136 | 137 | Height and width in pixels to be applied to node before rendering. 138 | 139 | #### style 140 | 141 | An object whose properties to be copied to node's style before rendering. 142 | You might want to check [this reference](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference) 143 | for JavaScript names of CSS properties. 144 | 145 | #### quality 146 | 147 | A number between 0 and 1 indicating image quality (e.g. 0.92 => 92%) of the 148 | JPEG image. Defaults to 1.0 (100%) 149 | 150 | #### cacheBust 151 | 152 | Set to true to append the current time as a query string to URL requests to enable cache busting. Defaults to false 153 | 154 | #### imagePlaceholder 155 | 156 | A data URL for a placeholder image that will be used when fetching an image fails. Defaults to undefined and will throw an error on failed images 157 | 158 | ## Browsers 159 | 160 | It's tested on latest Chrome and Firefox (49 and 45 respectively at the time 161 | of writing), with Chrome performing significantly better on big DOM trees, 162 | possibly due to it's more performant SVG support, and the fact that it supports 163 | `CSSStyleDeclaration.cssText` property. 164 | 165 | _Internet Explorer is not (and will not be) supported, as it does not support 166 | SVG `` tag_ 167 | 168 | _Safari [is not supported](https://github.com/tsayen/dom-to-image/issues/27), as it uses a stricter security model on ` tag. Suggested workaround is to use `toSvg` and render on the server._` 169 | 170 | ## Dependencies 171 | 172 | ### Source 173 | 174 | Only standard lib is currently used, but make sure your browser supports: 175 | 176 | - [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) 177 | - SVG `` tag 178 | 179 | ### Tests 180 | 181 | Most importantly, tests depend on: 182 | 183 | - [js-imagediff](https://github.com/HumbleSoftware/js-imagediff), 184 | to compare rendered and control images 185 | 186 | - [ocrad.js](https://github.com/antimatter15/ocrad.js), for the 187 | parts when you can't compare images (due to the browser 188 | rendering differences) and just have to test whether the text is rendered 189 | 190 | ## How it works 191 | 192 | There might some day exist (or maybe already exists?) a simple and standard 193 | way of exporting parts of the HTML to image (and then this script can only 194 | serve as an evidence of all the hoops I had to jump through in order to get 195 | such obvious thing done) but I haven't found one so far. 196 | 197 | This library uses a feature of SVG that allows having arbitrary HTML content 198 | inside of the `` tag. So, in order to render that DOM node 199 | for you, following steps are taken: 200 | 201 | 1. Clone the original DOM node recursively 202 | 203 | 2. Compute the style for the node and each sub-node and copy it to 204 | corresponding clone 205 | 206 | - and don't forget to recreate pseudo-elements, as they are not 207 | cloned in any way, of course 208 | 209 | 3. Embed web fonts 210 | 211 | - find all the `@font-face` declarations that might represent web fonts 212 | 213 | - parse file URLs, download corresponding files 214 | 215 | - base64-encode and inline content as `data:` URLs 216 | 217 | - concatenate all the processed CSS rules and put them into one ` 187 |
188 |
189 |

190 | Spotify was developed as an open source app powered by the 191 | Spotify/Last.fm/Apple Music Web API. By choosing to use this app, you 192 | agree to the use of your Spotify account username and data for your top 193 | artists and tracks. 194 |

195 |
196 |

197 | None of the data used by Receiptify is stored or collected anywhere, and 198 | it is NOT shared with any third parties. All information is used solely 199 | for displaying your Receipt. 200 |

201 |
202 |

203 | Although you can rest assured that your data is not being stored or used 204 | maliciously, if you would like to revoke Receiptify's permissions, you 205 | can visit 206 | your apps page 210 | and click "REMOVE ACCESS" on Receiptify. 211 | Here 214 | is a more detailed guide for doing so. 215 |

216 | 217 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Anonymous+Pro&display=swap'); 3 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); 4 | 5 | @font-face { 6 | font-family: 'receipt'; 7 | src: url('Merchant_Copy.ttf') format('truetype'); 8 | size-adjust: 115%; 9 | } 10 | textarea, 11 | input.text, 12 | input[type='text'], 13 | input[type='button'], 14 | input[type='submit'], 15 | .input-checkbox, 16 | select { 17 | -webkit-appearance: none; 18 | } 19 | select { 20 | background-image: url("data:image/svg+xml;utf8,"); 21 | background-repeat: no-repeat; 22 | background-position-x: 100%; 23 | background-position-y: 50%; 24 | } 25 | body { 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | width: 100%; 30 | } 31 | 32 | .mobile-ad { 33 | display: none; 34 | } 35 | label { 36 | word-break: break-all; 37 | } 38 | 39 | h1 { 40 | font-family: Inter, 'Helvetica Neue', sans-serif; 41 | font-weight: 600; 42 | font-size: 4.5rem; 43 | text-align: center; 44 | margin-bottom: 0px; 45 | margin-top: 0px; 46 | } 47 | h2 { 48 | font-size: 2.5rem; 49 | margin-top: 0px; 50 | } 51 | .container h2 { 52 | text-align: center; 53 | } 54 | p { 55 | font-family: Helvetica, Geneva, Tahoma, sans-serif; 56 | } 57 | .logo { 58 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 59 | font-weight: 600; 60 | font-size: 3.3rem; 61 | text-align: center; 62 | padding-top: 15px; 63 | word-break: break-word; 64 | } 65 | .info { 66 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 67 | text-align: center; 68 | } 69 | .period { 70 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 71 | font-size: 1.5rem; 72 | text-align: center; 73 | padding-bottom: 15px; 74 | } 75 | 76 | .temp { 77 | width: 100%; 78 | height: 50px; 79 | background-color: #ff0000; 80 | padding: 0 30%; 81 | } 82 | 83 | #home_vrec_1, 84 | #home_vrec_2, 85 | #lastfm_vrec_1, 86 | #lastfm_vrec_2 { 87 | position: sticky; 88 | position: -webkit-sticky; 89 | left: 0; 90 | top: 20px; 91 | } 92 | 93 | #home_vrec_2, 94 | #lastfm_vrec_2 { 95 | right: 0; 96 | left: auto; 97 | } 98 | 99 | .container { 100 | width: 100%; 101 | margin: auto; 102 | display: flex; 103 | flex-direction: column; 104 | justify-content: center; 105 | } 106 | .container > div { 107 | width: 65%; 108 | margin: auto; 109 | } 110 | .sticky-container { 111 | position: absolute; 112 | height: 100% !important; 113 | width: 15%; 114 | left: 2%; 115 | } 116 | .sticky-container__right { 117 | right: 2%; 118 | left: auto; 119 | } 120 | .page-container { 121 | margin: auto; 122 | margin-top: 80px; 123 | width: 100%; 124 | } 125 | .content-container { 126 | width: 100%; 127 | /* position: absolute; */ 128 | margin: auto; 129 | } 130 | .login-btn { 131 | margin: 2vh auto; 132 | text-align: center; 133 | text-decoration: none; 134 | padding: 2vh 3vh; 135 | background-color: #1db954; 136 | font-size: 20px; 137 | border: none; 138 | cursor: pointer; 139 | border-radius: 10px; 140 | color: white; 141 | font-family: 'Helvetica Neue', Helvetica, Geneva, Tahoma, sans-serif; 142 | width: 100%; 143 | display: block; 144 | max-width: 295px; 145 | } 146 | 147 | .footer p { 148 | font-size: 1.5rem; 149 | } 150 | 151 | .apple { 152 | background-color: #f94c57; 153 | } 154 | 155 | .lastfm { 156 | background-color: black; 157 | } 158 | 159 | .logo { 160 | text-transform: uppercase; 161 | } 162 | 163 | .time-btn { 164 | border: none; 165 | text-align: center; 166 | text-decoration: none; 167 | padding: 1vh 5vh; 168 | background-color: #0056ff; 169 | font-size: 20px; 170 | cursor: pointer; 171 | border-radius: 10px; 172 | color: white; 173 | font-family: 'Helvetica Neue', Helvetica, Geneva, Tahoma, sans-serif; 174 | margin: 0vh 1vh; 175 | width: auto; 176 | } 177 | .login-btn:hover { 178 | text-decoration: none; 179 | color: white; 180 | } 181 | #options, 182 | #num-options, 183 | #appleoption { 184 | margin: 0px auto; 185 | margin-top: 1vh; 186 | display: flex; 187 | flex-direction: row; 188 | justify-content: center; 189 | } 190 | .flex-center { 191 | justify-content: center; 192 | display: flex; 193 | } 194 | .hidden { 195 | display: none; 196 | } 197 | #loggedin, 198 | #login { 199 | display: flex; 200 | flex-direction: column; 201 | align-items: center; 202 | } 203 | .hidden { 204 | display: none; 205 | } 206 | .date { 207 | margin: 0vh; 208 | } 209 | .tracks { 210 | margin: 5px 0vh; 211 | } 212 | .new-feature { 213 | margin-left: 3px; 214 | padding: 2px 4px; 215 | border-radius: 5px; 216 | background-color: gold; 217 | color: black; 218 | font-size: 14px; 219 | font-weight: bold; 220 | } 221 | #search-form { 222 | margin-top: 2vh; 223 | display: flex; 224 | flex-direction: column; 225 | } 226 | #searchBox, 227 | #custom-name { 228 | padding: 8px 10px; 229 | border: 2px solid black; 230 | font-size: 18px; 231 | border-radius: 10px; 232 | color: black; 233 | outline: none; 234 | } 235 | #custom-name { 236 | margin-bottom: 10px; 237 | } 238 | #receipt { 239 | clear: both; 240 | display: flex; 241 | flex-direction: column; 242 | align-items: center; 243 | align-content: center; 244 | justify-content: center; 245 | } 246 | #receipt a, 247 | #receipt a:visited { 248 | color: black; 249 | text-decoration: none !important; 250 | } 251 | .under { 252 | display: flex; 253 | flex-direction: column; 254 | justify-content: center; 255 | align-content: center; 256 | align-items: center; 257 | margin-top: 10px; 258 | width: 100%; 259 | } 260 | .under button { 261 | width: 100%; 262 | margin-bottom: 10px; 263 | } 264 | .username-input { 265 | display: flex; 266 | flex-direction: row; 267 | justify-content: center; 268 | align-items: center; 269 | padding: 2vh; 270 | } 271 | .username-input * { 272 | margin: 2%; 273 | } 274 | .receiptContainer { 275 | background-image: url('wrinkled-paper-texture-7.jpg'); 276 | background-position: center; 277 | width: 340px; 278 | filter: brightness(105%); 279 | padding: 20px; 280 | text-transform: uppercase; 281 | } 282 | table { 283 | width: 100%; 284 | } 285 | .receiptContainerSmaller .name { 286 | width: 180px; 287 | } 288 | .begin { 289 | padding-left: 0vh; 290 | } 291 | #download { 292 | margin-top: 2vh; 293 | } 294 | p, 295 | table, 296 | .date { 297 | font-family: 'receipt', 'Anonymous Pro', 'Courier New', Courier, monospace; 298 | font-size: 2rem; 299 | line-height: 2rem; 300 | } 301 | 302 | .anonymous p, 303 | .anonymous table, 304 | .anonymous .date, 305 | .anonymous .smaller, 306 | .anonymous .latin { 307 | font-family: 'Anonymous Pro', 'Courier New', Courier, monospace; 308 | font-size: 1.75rem; 309 | } 310 | 311 | .latin { 312 | font-size: 1.9rem; 313 | } 314 | .smaller { 315 | font-size: 1.45rem; 316 | } 317 | 318 | #login p { 319 | font-size: 1.5rem; 320 | } 321 | 322 | table { 323 | border: none; 324 | } 325 | .total-counts { 326 | border-top: 1px dashed; 327 | } 328 | .total-counts-end { 329 | border-bottom: 1px dashed; 330 | } 331 | thead { 332 | border-top: 1px dashed; 333 | border-bottom: 1px dashed; 334 | } 335 | td { 336 | padding: 2px 10px; 337 | vertical-align: top; 338 | } 339 | .name { 340 | width: 225px; 341 | overflow: auto; 342 | } 343 | .length { 344 | text-align: right; 345 | padding-left: 10px; 346 | padding-right: 0px; 347 | } 348 | .website { 349 | margin-bottom: 0px; 350 | text-transform: lowercase; 351 | } 352 | .thanks { 353 | text-align: center; 354 | padding: 15px 0px; 355 | } 356 | .thanks img { 357 | width: 70%; 358 | } 359 | .spotify-logo { 360 | margin-top: 20px; 361 | width: 80px !important; 362 | } 363 | .main-body { 364 | display: flex; 365 | flex-direction: row; 366 | justify-content: flex-start; 367 | align-items: flex-start; 368 | width: 100%; 369 | padding-top: 2vh; 370 | } 371 | .receipt-wrapper { 372 | width: 50%; 373 | justify-content: flex-end; 374 | display: flex; 375 | } 376 | .customize { 377 | min-width: 30%; 378 | max-width: 50%; 379 | padding-left: 2vw; 380 | align-self: stretch; 381 | } 382 | .customize > p, 383 | #explanation p, 384 | #track-edit p { 385 | font-family: Inter, 'Helvetica Neue', sans-serif; 386 | line-height: 1.2; 387 | } 388 | #track-edit { 389 | margin-top: 30px; 390 | } 391 | #track-edit div { 392 | display: flex; 393 | justify-content: space-between; 394 | padding: 10px 20px; 395 | margin-bottom: 5px; 396 | border-radius: 10px; 397 | align-items: center; 398 | } 399 | #track-edit p { 400 | margin: 0px; 401 | } 402 | #track-edit div:hover { 403 | background: #e2e2e2; 404 | cursor: pointer; 405 | } 406 | .customize-header { 407 | font-weight: 700; 408 | font-size: 36px; 409 | padding-bottom: 1.5vh; 410 | } 411 | .explanation-header { 412 | padding-top: 2.5vh; 413 | } 414 | #type-select-dropdown { 415 | width: 100%; 416 | padding: 8px 10px; 417 | border: 2px solid black; 418 | border-radius: 10px; 419 | outline: none; 420 | font-size: 18px; 421 | } 422 | #top-type { 423 | margin: 0; 424 | } 425 | #options-header, 426 | #num-header, 427 | #font-header { 428 | margin-top: 20px; 429 | } 430 | .classic { 431 | font-family: 'receipt', 'Anonymous Pro', 'Courier New', Courier, monospace; 432 | } 433 | .international { 434 | font-family: 'Anonymous Pro', 'Courier New', Courier, monospace; 435 | } 436 | /** -- Added for Anthems Partnership --**/ 437 | .header-section { 438 | margin-top: 20px; 439 | width: 964px; 440 | } 441 | 442 | .seperator { 443 | width: 100%; 444 | padding: 0 30%; 445 | } 446 | 447 | .seperator hr { 448 | width: 100%; 449 | border-top: 1px solid #e5e5e5; 450 | } 451 | 452 | .btn-group { 453 | position: relative; 454 | display: inline-flex; 455 | vertical-align: middle; 456 | } 457 | .btn-group > .btn:first-of-type { 458 | border-top-left-radius: 4px !important; 459 | border-bottom-left-radius: 4px !important; 460 | } 461 | .btn-check { 462 | position: absolute; 463 | clip: rect(0, 0, 0, 0); 464 | pointer-events: none; 465 | } 466 | input[type='radio' i] { 467 | background-color: initial; 468 | cursor: default; 469 | appearance: auto; 470 | padding: initial; 471 | border: initial; 472 | } 473 | #start-searching { 474 | display: none; 475 | text-align: center; 476 | font-family: Inter, 'Helvetica Neue'; 477 | font-size: 24px; 478 | padding: 2vh 2vw; 479 | background-color: #e2e2e2; 480 | border-radius: 10px; 481 | } 482 | .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { 483 | border-radius: 10px !important; 484 | } 485 | .customize .btn { 486 | font-weight: 500; 487 | border-radius: 10px !important; 488 | border: 2px solid black !important; 489 | color: black !important; 490 | font-size: 18px !important; 491 | margin: 0px 8px; 492 | } 493 | .customize .btn-outline-secondary:hover, 494 | .customize .btn-check:checked + .btn-outline-secondary { 495 | background-color: black !important; 496 | color: white !important; 497 | } 498 | .btn-group > .btn-group:not(:first-child), 499 | .btn-group > .btn:not(:first-child) { 500 | margin-left: -1px; 501 | } 502 | .btn-group-vertical > .btn, 503 | .btn-group > .btn { 504 | position: relative; 505 | flex: 1 1 auto; 506 | } 507 | .btn-outline-primary { 508 | color: #0d6efd; 509 | border-color: #0d6efd; 510 | } 511 | .btn-outline-primary:hover, 512 | .btn-check:checked + .btn-outline-primary { 513 | color: #fff; 514 | background-color: #0d6efd; 515 | border-color: #0d6efd; 516 | } 517 | .btn-outline-secondary { 518 | color: #6c757d; 519 | border-color: #6c757d; 520 | } 521 | .btn-outline-secondary:hover, 522 | .btn-check:checked + .btn-outline-secondary { 523 | color: #fff; 524 | background-color: #6c757d; 525 | border-color: #6c757d; 526 | } 527 | 528 | .about { 529 | width: 70%; 530 | text-align: center; 531 | padding: 5vh 0; 532 | } 533 | 534 | .about p { 535 | font-family: 'Inter'; 536 | font-size: 16px; 537 | } 538 | 539 | .about h2 { 540 | font-size: 20px; 541 | } 542 | 543 | .middle-container { 544 | text-align: center; 545 | } 546 | 547 | .middle-container .title { 548 | font-family: 'Helvetica Neue'; 549 | font-size: 32px; 550 | font-weight: bold; 551 | padding: 24px 2px 16px 2px; 552 | } 553 | 554 | .middle-container .subtitle { 555 | font-family: 'Helvetica Neue'; 556 | font-size: 18px; 557 | max-width: 400px; 558 | margin: 0 auto; 559 | padding: 0 8px; 560 | } 561 | 562 | .store-icons { 563 | width: 284.74px; 564 | height: 40px; 565 | margin: 16px; 566 | cursor: pointer; 567 | } 568 | 569 | .anthems-logo { 570 | width: 178.04px; 571 | height: 40px; 572 | margin: 16px; 573 | cursor: pointer; 574 | } 575 | 576 | .mixes-promo { 577 | width: 368px; 578 | height: 294.49px; 579 | background-image: url(assets/img/mixes-promo.jpg); 580 | background-repeat: no-repeat; 581 | background-size: contain; 582 | margin: 0 auto; 583 | margin-bottom: 55px; 584 | cursor: pointer; 585 | } 586 | /** -- End of Added for Anthems Partnership --**/ 587 | 588 | /* NAV BEGIN */ 589 | 590 | /* Navigation Bar */ 591 | .nav-logo-p { 592 | font-size: 3.7vh; 593 | left: 4vw; 594 | margin: 0; 595 | position: absolute; 596 | top: 50%; 597 | -ms-transform: translateY(-50%); 598 | transform: translateY(-50%); 599 | } 600 | nav { 601 | width: 100%; 602 | box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.07); 603 | -webkit-box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.07); 604 | -moz-box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.07); 605 | height: auto; 606 | } 607 | nav > ul { 608 | display: inline-block; 609 | list-style-type: none; 610 | padding: 2vh 3vw; 611 | margin: 0vh; 612 | float: right; 613 | } 614 | nav > ul > li { 615 | border: 1px solid transparent; 616 | border-radius: 1vh; 617 | padding: 1vh 1.2vw; 618 | margin: 0; 619 | display: inline-block; 620 | } 621 | nav > ul > li:hover { 622 | border-radius: 1.5vh; 623 | -webkit-transition-duration: 0.5s; 624 | -o-transition-duration: 0.5s; 625 | transition-duration: 0.5s; 626 | color: white; 627 | text-decoration: none; 628 | } 629 | nav a { 630 | text-decoration: none; 631 | } 632 | nav a:hover { 633 | text-decoration: none !important; 634 | } 635 | nav > ul > li > a { 636 | font-size: 18px; 637 | } 638 | .navTransparent { 639 | -webkit-transition-duration: 0.5s; 640 | -o-transition-duration: 0.5s; 641 | transition-duration: 0.5s; 642 | background-color: white; 643 | } 644 | .navTransparent > ul > li:hover { 645 | border: 1px solid white; 646 | } 647 | .navTransparent > ul > li > a { 648 | -webkit-transition-duration: 0.5s; 649 | -o-transition-duration: 0.5s; 650 | transition-duration: 0.5s; 651 | color: white; 652 | font-size: 2.5vh; 653 | } 654 | .navTransparent > a { 655 | -webkit-transition-duration: 0.5s; 656 | -o-transition-duration: 0.5s; 657 | transition-duration: 0.5s; 658 | color: white; 659 | font-family: var(--heading-font); 660 | } 661 | .navColor { 662 | -webkit-transition-duration: 0.5s; 663 | -o-transition-duration: 0.5s; 664 | transition-duration: 0.5s; 665 | background-color: white; 666 | z-index: 99; 667 | } 668 | .navColor > ul > li:hover { 669 | background-color: #1db954; 670 | } 671 | #logout-btn:hover { 672 | background-color: transparent !important; 673 | } 674 | .navColor > ul > li:hover a { 675 | color: white; 676 | } 677 | .navColor > ul > li > a { 678 | -webkit-transition-duration: 0.5s; 679 | -o-transition-duration: 0.5s; 680 | transition-duration: 0.5s; 681 | color: black; 682 | } 683 | .nav-logo-p { 684 | font-family: var(--heading-font); 685 | -webkit-transition-duration: 0.5s; 686 | -o-transition-duration: 0.5s; 687 | transition-duration: 0.5s; 688 | color: white; 689 | } 690 | 691 | .ad-centered { 692 | justify-content: center; 693 | } 694 | 695 | .options-ad { 696 | display: flex; 697 | height: 100px; 698 | width: 720px; 699 | } 700 | 701 | .navColor .hamburger-menu { 702 | display: none; 703 | flex-direction: column; 704 | cursor: pointer; 705 | align-items: flex-end; 706 | padding-right: 10px; 707 | } 708 | 709 | .navColor .hamburger-menu span { 710 | background-color: grey; 711 | margin: 3px 0; 712 | height: 3px; 713 | width: 25px; 714 | border-radius: 3px; 715 | } 716 | 717 | @media (max-width: 330px) { 718 | .options-ad { 719 | display: inline-block; 720 | width: 320px; 721 | } 722 | } 723 | 724 | .mobile-ad { 725 | padding: 2vh 0; 726 | } 727 | 728 | /* #home_header { 729 | height: 50px; 730 | width: 100%; 731 | background-color: pink; 732 | } */ 733 | 734 | /* NAV END */ 735 | @media only screen and (min-width: 768px) and (max-width: 1280px) { 736 | .main-body { 737 | width: 70%; 738 | } 739 | #loggedin, 740 | #login { 741 | width: 100%; 742 | } 743 | .container { 744 | width: 100%; 745 | } 746 | .about { 747 | width: 80%; 748 | } 749 | } 750 | @media only screen and (max-width: 1280px) { 751 | .desktop-ad { 752 | display: none; 753 | } 754 | .mobile-ad { 755 | display: block; 756 | } 757 | #options, 758 | #font-options { 759 | flex-wrap: wrap; 760 | } 761 | #options label, 762 | #font-options label { 763 | margin-top: 10px; 764 | } 765 | .navColor { 766 | box-shadow: none; 767 | -webkit-box-shadow: none; 768 | -moz-box-shadow: none; 769 | } 770 | .navColor > ul { 771 | display: none; 772 | } 773 | .navColor > ul.show { 774 | width: 100%; 775 | display: flex; 776 | position: absolute; 777 | background-color: white; 778 | box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.07); 779 | -webkit-box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.07); 780 | -moz-box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.07); 781 | z-index: 9999; 782 | } 783 | .navColor a { 784 | width: 100% !important; 785 | } 786 | .navColor ul.show li { 787 | width: 100%; 788 | text-align: right; 789 | } 790 | .navColor .hamburger-menu { 791 | display: flex; 792 | } 793 | .receipt-wrapper { 794 | justify-content: center; 795 | width: 100%; 796 | max-width: 100%; 797 | margin-bottom: 30px; 798 | } 799 | .customize { 800 | width: 100%; 801 | max-width: 100%; 802 | } 803 | .main-body { 804 | flex-direction: column; 805 | } 806 | .swsh-banner--desktop { 807 | display: none; 808 | } 809 | .swsh-banner--mobile { 810 | display: block; 811 | } 812 | #home_vrec_1, 813 | #home_vrec_2, 814 | #lastfm_vrec_1, 815 | #lastfm_vrec_2, 816 | .sticky-container { 817 | display: none; 818 | } 819 | #loggedin, 820 | #login { 821 | width: 100%; 822 | } 823 | nav { 824 | margin-top: 40px; 825 | } 826 | .time-btn { 827 | margin: 1vh auto; 828 | } 829 | .btns { 830 | flex-direction: column; 831 | } 832 | .container { 833 | width: 100%; 834 | } 835 | .about { 836 | width: 90%; 837 | } 838 | } 839 | 840 | @media (max-width: 900px) { 841 | .header-section { 842 | width: 100%; 843 | padding: 0 32px; 844 | } 845 | 846 | .seperator { 847 | padding: 0; 848 | } 849 | 850 | .mixes-promo { 851 | background-image: url(assets/img/mixes-promo-mobile.jpg); 852 | width: auto; 853 | height: 190.96px; 854 | max-width: 337px; 855 | } 856 | 857 | .middle-container { 858 | padding: 0 20px; 859 | } 860 | } 861 | 862 | @media (max-width: 330px) { 863 | .header-section { 864 | padding: 0 15px; 865 | } 866 | } 867 | -------------------------------------------------------------------------------- /public/tcf.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function makeStub() { 3 | var TCF_LOCATOR_NAME = '__tcfapiLocator'; 4 | var queue = []; 5 | var win = window; 6 | var cmpFrame; 7 | 8 | function addFrame() { 9 | var doc = win.document; 10 | var otherCMP = !!win.frames[TCF_LOCATOR_NAME]; 11 | 12 | if (!otherCMP) { 13 | if (doc.body) { 14 | var iframe = doc.createElement('iframe'); 15 | 16 | iframe.style.cssText = 'display:none'; 17 | iframe.name = TCF_LOCATOR_NAME; 18 | doc.body.appendChild(iframe); 19 | } else { 20 | setTimeout(addFrame, 5); 21 | } 22 | } 23 | return !otherCMP; 24 | } 25 | 26 | function tcfAPIHandler() { 27 | var gdprApplies; 28 | var args = arguments; 29 | 30 | if (!args.length) { 31 | return queue; 32 | } else if (args[0] === 'setGdprApplies') { 33 | if (args.length > 3 && args[2] === 2 && typeof args[3] === 'boolean') { 34 | gdprApplies = args[3]; 35 | if (typeof args[2] === 'function') { 36 | args[2]('set', true); 37 | } 38 | } 39 | } else if (args[0] === 'ping') { 40 | var retr = { 41 | gdprApplies: gdprApplies, 42 | cmpLoaded: false, 43 | cmpStatus: 'stub', 44 | }; 45 | 46 | if (typeof args[2] === 'function') { 47 | args[2](retr); 48 | } 49 | } else { 50 | queue.push(args); 51 | } 52 | } 53 | 54 | function postMessageEventHandler(event) { 55 | var msgIsString = typeof event.data === 'string'; 56 | var json = {}; 57 | 58 | try { 59 | if (msgIsString) { 60 | json = JSON.parse(event.data); 61 | } else { 62 | json = event.data; 63 | } 64 | } catch (ignore) {} 65 | 66 | var payload = json.__tcfapiCall; 67 | 68 | if (payload) { 69 | window.__tcfapi( 70 | payload.command, 71 | payload.version, 72 | function (retValue, success) { 73 | var returnMsg = { 74 | __tcfapiReturn: { 75 | returnValue: retValue, 76 | success: success, 77 | callId: payload.callId, 78 | }, 79 | }; 80 | if (msgIsString) { 81 | returnMsg = JSON.stringify(returnMsg); 82 | } 83 | if (event && event.source && event.source.postMessage) { 84 | event.source.postMessage(returnMsg, '*'); 85 | } 86 | }, 87 | payload.parameter 88 | ); 89 | } 90 | } 91 | 92 | while (win) { 93 | try { 94 | if (win.frames[TCF_LOCATOR_NAME]) { 95 | cmpFrame = win; 96 | break; 97 | } 98 | } catch (ignore) {} 99 | 100 | if (win === window.top) { 101 | break; 102 | } 103 | win = win.parent; 104 | } 105 | if (!cmpFrame) { 106 | addFrame(); 107 | win.__tcfapi = tcfAPIHandler; 108 | win.addEventListener('message', postMessageEventHandler, false); 109 | } 110 | } 111 | 112 | makeStub(); 113 | })(); 114 | -------------------------------------------------------------------------------- /public/wrinkled-paper-texture-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michellexliu/receiptify/cbddda52dc0bc37c07e28884382270526c5221d8/public/wrinkled-paper-texture-7.jpg -------------------------------------------------------------------------------- /views/applemusic.handlebars: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michellexliu/receiptify/cbddda52dc0bc37c07e28884382270526c5221d8/views/applemusic.handlebars -------------------------------------------------------------------------------- /views/footer.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /views/header.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Receiptify 7 | 8 | 9 | 10 | 11 |
12 |
13 |

Receiptify

14 |

Top Track Generator

-------------------------------------------------------------------------------- /views/home.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Receiptify 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Receiptify

16 |

Top Track Generator

17 | 18 | 19 | 20 |

21 | This website was inspired by the Instagram account 22 | @albumreceipts! 23 |

24 |

25 | Made by 26 | Michelle Liu. 27 |

28 |

29 | PayPal | 30 | Venmo 31 |

32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /views/lastfm.handlebars: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michellexliu/receiptify/cbddda52dc0bc37c07e28884382270526c5221d8/views/lastfm.handlebars -------------------------------------------------------------------------------- /views/spotify.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Receiptify 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Receiptify

15 |

Top Track Generator

16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | 26 |

27 | {{shortTerm.period}} 28 |

29 |

30 | ORDER #000{{shortTerm.num}} FOR {{shortTerm.name}} 31 |

32 |

33 | {{shortTerm.time}} 34 |

35 | 36 | 37 | 40 | 43 | 46 | 47 | {{#each shortTerm.tracks}} 48 | 49 | 52 | 60 | 63 | 64 | {{/each}} 65 | 66 | 69 | 72 | 73 | 74 | 77 | 80 | 81 |
38 | QTY 39 | 41 | ITEM 42 | 44 | AMT 45 |
50 | {{this.id}} 51 | 53 | {{this.name}} - 54 | {{#each this.artists}} 55 | 56 | {{this.name}} 57 | 58 | {{/each}} 59 | 61 | {{this.duration_ms}} 62 |
67 | ITEM COUNT: 68 | 70 | 10 71 |
75 | TOTAL: 76 | 78 | {{shortTerm.total}} 79 |
82 |

83 | CARD #: **** **** **** 2023 84 |

85 |

86 | AUTH CODE: 123421 87 |

88 |

89 | CARDHOLDER: {{shortTerm.name}} 90 |

91 |
92 |

93 | THANK YOU FOR VISITING! 94 |

95 | 96 |

97 | receiptify.herokuapp.com 98 |

99 |
100 |
101 |
102 | 105 |

106 | {{mediumTerm.period}} 107 |

108 |

109 | ORDER #000{{mediumTerm.num}} FOR {{mediumTerm.name}} 110 |

111 |

112 | {{mediumTerm.time}} 113 |

114 | 115 | 116 | 119 | 122 | 125 | 126 | {{#each mediumTerm.tracks}} 127 | 128 | 131 | 139 | 142 | 143 | {{/each}} 144 | 145 | 148 | 151 | 152 | 153 | 156 | 159 | 160 |
117 | QTY 118 | 120 | ITEM 121 | 123 | AMT 124 |
129 | {{this.id}} 130 | 132 | {{this.name}} - 133 | {{#each this.artists}} 134 | 135 | {{this.name}} 136 | 137 | {{/each}} 138 | 140 | {{this.duration_ms}} 141 |
146 | ITEM COUNT: 147 | 149 | 10 150 |
154 | TOTAL: 155 | 157 | {{mediumTerm.total}} 158 |
161 |

162 | CARD #: **** **** **** 2023 163 |

164 |

165 | AUTH CODE: 123421 166 |

167 |

168 | CARDHOLDER: {{mediumTerm.name}} 169 |

170 |
171 |

172 | THANK YOU FOR VISITING! 173 |

174 | 175 |

176 | receiptify.herokuapp.com 177 |

178 |
179 |
180 |
181 | 184 |

185 | {{longTerm.period}} 186 |

187 |

188 | ORDER #000{{longTerm.num}} FOR {{longTerm.name}} 189 |

190 |

191 | {{longTerm.time}} 192 |

193 | 194 | 195 | 198 | 201 | 204 | 205 | {{#each longTerm.tracks}} 206 | 207 | 210 | 218 | 221 | 222 | {{/each}} 223 | 224 | 227 | 230 | 231 | 232 | 235 | 238 | 239 |
196 | QTY 197 | 199 | ITEM 200 | 202 | AMT 203 |
208 | {{this.id}} 209 | 211 | {{this.name}} - 212 | {{#each this.artists}} 213 | 214 | {{this.name}} 215 | 216 | {{/each}} 217 | 219 | {{this.duration_ms}} 220 |
225 | ITEM COUNT: 226 | 228 | 10 229 |
233 | TOTAL: 234 | 236 | {{longTerm.total}} 237 |
240 |

241 | CARD #: **** **** **** 2023 242 |

243 |

244 | AUTH CODE: 123421 245 |

246 |

247 | CARDHOLDER: {{longTerm.name}} 248 |

249 |
250 |

251 | THANK YOU FOR VISITING! 252 |

253 | 254 |

255 | receiptify.herokuapp.com 256 |

257 |
258 |
259 | 262 |
263 |
264 | 265 |
266 |
267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /views/spotifyReceipt.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Receiptify 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Receiptify

16 |

Top Track Generator

17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 | 28 |

29 | {{period}} 30 |

31 |

32 | ORDER #000{{num}} FOR {{name}} 33 |

34 |

35 | {{time}} 36 |

37 | 38 | 39 | 42 | 45 | 48 | 49 | {{#each tracks}} 50 | 51 | 54 | 62 | 65 | 66 | {{/each}} 67 | 68 | 71 | 74 | 75 | 76 | 79 | 82 | 83 |
40 | QTY 41 | 43 | ITEM 44 | 46 | AMT 47 |
52 | {{this.id}} 53 | 55 | {{this.name}} - 56 | {{#each this.artists}} 57 | 58 | {{this.name}} 59 | 60 | {{/each}} 61 | 63 | {{this.duration_ms}} 64 |
69 | ITEM COUNT: 70 | 72 | 10 73 |
77 | TOTAL: 78 | 80 | {{total}} 81 |
84 |

85 | CARD #: **** **** **** 2023 86 |

87 |

88 | AUTH CODE: 123421 89 |

90 |

91 | CARDHOLDER: {{name}} 92 |

93 |
94 |

95 | THANK YOU FOR VISITING! 96 |

97 | 98 |

99 | receiptify.herokuapp.com 100 |

101 |
102 |
103 |
104 |
105 |
106 |
107 | 110 | 111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | --------------------------------------------------------------------------------