├── .gitignore ├── README.md ├── package.json ├── screenshot.gif ├── server.js ├── src ├── app.html ├── app.js ├── app.less ├── app.reducer.js ├── components │ ├── Demo │ │ ├── Demo.js │ │ └── Demo.less │ ├── Menu │ │ ├── Menu.js │ │ └── Menu.less │ └── Trigger │ │ ├── Trigger.js │ │ └── Trigger.less └── data │ └── fonts.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | src/public/app.packed.js 4 | src/public/app.packed.css 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fonts switcher (react, redux, webpack) 2 | 3 | Specify a list of Google fonts to load, and then toggle between each from a menu. 4 | 5 | ![screenshot](screenshot.gif) 6 | 7 | ### Install 8 | First clone or download the app to your local machine, then install all dependencies with: 9 | ```sh 10 | $ npm install 11 | ``` 12 | 13 | ### Run 14 | Running the app is an easy few steps after you've installed it. First boot the express server that is included by running: 15 | ```sh 16 | $ npm start 17 | ``` 18 | You should be able to view the app in your web browser at: http://localhost:8080. 19 | 20 | ### Dev 21 | Want to contribute? Great! 22 | 23 | To develop, you'll need to make sure that webpack is watching for changes that you save so that it can auto bundle everything together. 24 | To set up a webpack watcher to bundle upon each save: 25 | ```sh 26 | $ npm run dev 27 | ``` 28 | To bundle one time and NOT run a watch: 29 | ```sh 30 | $ npm run build 31 | ``` 32 | 33 | Hope you enjoy, cheers! 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontswitcher", 3 | "version": "1.0.0", 4 | "description": "React font switcher", 5 | "main": "app/app.js", 6 | "scripts": { 7 | "dev": "webpack --watch", 8 | "build": "webpack -p" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "emojiart" 13 | }, 14 | "keywords": [ 15 | "letters", 16 | "emoji art", 17 | "app" 18 | ], 19 | "author": "Michael Dick", 20 | "license": "ISC", 21 | "dependencies": { 22 | "express": "^4.13.4", 23 | "extract-text-webpack-plugin": "^1.0.1", 24 | "react": "^15.0.1", 25 | "react-dom": "^15.0.1", 26 | "react-redux": "^4.4.5", 27 | "react-router": "^2.3.0", 28 | "redux": "^3.5.2", 29 | "webfontloader": "^1.6.24" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.7.7", 33 | "babel-loader": "^6.2.4", 34 | "babel-preset-es2015": "^6.6.0", 35 | "babel-preset-react": "^6.5.0", 36 | "babel-preset-stage-2": "^6.5.0", 37 | "css-loader": "^0.23.1", 38 | "json-loader": "^0.5.4", 39 | "less": "^2.6.1", 40 | "less-loader": "^2.2.3", 41 | "style-loader": "^0.13.1", 42 | "webpack": "^1.12.14" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miked1ck/fontswitcher/7c1f16597d140c8e3a52359de4474b345e3a286d/screenshot.gif -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | // Serve static shit 5 | app.use('/public', express.static(__dirname + '/src/public')); 6 | app.get("*", function(req, res) { 7 | res.sendfile('./src/app.html') 8 | }); 9 | 10 | // Run it 11 | app.listen(8080, function () { 12 | console.log('Fontswitcher server is listening at http://localhost:8080'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Font switcher 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {createStore} from 'redux'; 4 | import {Provider} from 'react-redux'; 5 | import WebFont from 'webfontloader'; 6 | 7 | 8 | // Load fonts 9 | /////////////////////////// 10 | 11 | import {fonts} from './data/fonts.json'; 12 | 13 | 14 | // Components 15 | /////////////////////////// 16 | 17 | import Trigger from './components/Trigger/Trigger.js'; 18 | import Menu from './components/Menu/Menu.js'; 19 | import Demo from './components/Demo/Demo.js'; 20 | 21 | 22 | // App Reducer 23 | /////////////////////////// 24 | 25 | import AppReducer from './app.reducer.js'; 26 | 27 | 28 | // App Component 29 | /////////////////////////// 30 | 31 | class App extends React.Component { 32 | constructor() { 33 | super(); 34 | 35 | const fonts_to_load = fonts.filter(function(font) { 36 | return font.needs_loaded; 37 | }).map(function(font) { 38 | return font.name.replace(' ', '+') + '::latin'; 39 | }); 40 | 41 | WebFont.load({ 42 | google: { families: fonts_to_load } 43 | }); 44 | } 45 | 46 | render() { 47 | return ( 48 | 49 |
50 |
51 | 52 | 53 |
54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | 63 | // Bind to DOM 64 | /////////////////////////// 65 | 66 | ReactDOM.render( 67 | , 68 | document.getElementById('app') 69 | ); 70 | -------------------------------------------------------------------------------- /src/app.less: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,form,label,table,caption,tbody,tfoot,thead,tr,th,td {margin: 0; padding: 0; border: 0; outline: 0; font-size: 100%; vertical-align: baseline; background: transparent; } 2 | body { line-height: 1; } 3 | ol, ul { list-style: none; } 4 | blockquote, q { quotes: none; } 5 | :focus { outline: 0; } 6 | ins { text-decoration: none; } 7 | del { text-decoration: line-through; } 8 | table { border-collapse: collapse; border-spacing: 0; } 9 | header, section, nav, footer, aside, article { display: block; } 10 | 11 | body { 12 | background: #f8fafb; 13 | } 14 | 15 | [hidden] { 16 | display: block; 17 | } 18 | 19 | 20 | // Parent switcher styles 21 | ////////////////////////////// 22 | 23 | .switcher { 24 | position: fixed; 25 | left: 20px; 26 | bottom: 20px; 27 | 28 | // namespaced here to only effect the switcher 29 | font-family: 'Helvetica Neue', arial, sans-serif; 30 | font-size: 20px; 31 | line-height: 1.4; 32 | } 33 | 34 | 35 | // Colors 36 | ////////////////////////////// 37 | 38 | @color_blue: hsla(220, 100%, 50%, 1); 39 | @color_gray: hsla(220, 30%, 90%, 1); 40 | @color_white: hsla(0, 100%, 100%, 1); 41 | @color_text: hsla(220, 10%, 50%, 1); 42 | 43 | 44 | // Levitation 45 | ///////////////////////////// 46 | 47 | @elevation_100: 5px 5px 10px hsla(0, 0, 0, .07); 48 | @elevation_200: 5px 5px 15px hsla(0, 0, 0, .07); 49 | @elevation_300: 5px 5px 20px hsla(0, 0, 0, .07); 50 | @elevation_100: 5px 5px 25px hsla(0, 0, 0, .07); 51 | 52 | 53 | // Components 54 | ///////////////////////////// 55 | 56 | @import "/components/Trigger/Trigger"; 57 | @import "/components/Menu/Menu"; 58 | @import "/components/Demo/Demo"; 59 | -------------------------------------------------------------------------------- /src/app.reducer.js: -------------------------------------------------------------------------------- 1 | import {fonts} from './data/fonts.json'; 2 | 3 | 4 | // Init state 5 | /////////////////////////// 6 | 7 | const init = { 8 | 'font': fonts[0].name, 9 | 'isMenuToggled': false 10 | }; 11 | 12 | 13 | // Actions 14 | /////////////////////////// 15 | 16 | const actions = { 17 | switch_font: function(state, action) { 18 | return { ...state, font: action.font }; 19 | }, 20 | 21 | toggle_menu: function(state, action) { 22 | return {...state, isMenuToggled: !state.isMenuToggled}; 23 | } 24 | }; 25 | 26 | 27 | // Reducer 28 | /////////////////////////// 29 | 30 | function reducer(state = init, action) { 31 | switch (action.type) { 32 | case 'SWITCH_FONT': 33 | return actions.switch_font(state, action); 34 | case 'TOGGLE_MENU': 35 | return actions.toggle_menu(state, action); 36 | default: 37 | return state; 38 | } 39 | } 40 | 41 | 42 | // Export 43 | /////////////////////////// 44 | 45 | export default reducer; 46 | -------------------------------------------------------------------------------- /src/components/Demo/Demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | 5 | // Component 6 | /////////////////////////// 7 | 8 | class Demo extends React.Component { 9 | render() { 10 | return ( 11 |
12 |

Shores of the cosmic ocean concept of the number one the ash of stellar alchemy permanence of the stars something incredible is waiting to be known prime number take root and flourish not a sunrise but a galaxyrise something incredible is waiting to be known vastness is bearable only through love permanence of the stars shores of the cosmic ocean a mote of dust suspended in a sunbeam, courage of our questions! Muse about. Kindling the energy hidden in matter? Light years hydrogen atoms, at the edge of forever, Euclid colonies star stuff harvesting star light Orion's sword star stuff harvesting star light colonies decipherment cosmic ocean explorations. Euclid explorations of brilliant syntheses, rings of Uranus white dwarf corpus callosum network of wormholes hearts of the stars.

13 |

Corpus callosum cosmic fugue intelligent beings. At the edge of forever. Vastness is bearable only through love Flatland galaxies quasar Vangelis Flatland Rig Veda billions upon billions gathered by gravity explorations, at the edge of forever not a sunrise but a galaxyrise! Consciousness, tendrils of gossamer clouds birth. Apollonius of Perga, Tunguska event as a patch of light, extraplanetary, at the edge of forever! Preserve and cherish that pale blue dot paroxysm of global death extraordinary claims require extraordinary evidence? Prime number!

14 |

The only home we've ever known decipherment encyclopaedia galactica finite but unbounded, realm of the galaxies Cambrian explosion, are creatures of the cosmos prime number, hearts of the stars brain is the seed of intelligence. Hypatia finite but unbounded. A mote of dust suspended in a sunbeam at the edge of forever Sea of Tranquility, bits of moving fluff permanence of the stars, a billion trillion cosmic ocean something incredible is waiting to be known radio telescope a still more glorious dawn awaits. Paroxysm of global death a still more glorious dawn awaits. As a patch of light vastness is bearable only through love Flatland, radio telescope extraordinary claims require extraordinary evidence the carbon in our apple pies shores of the cosmic ocean the sky calls to us circumnavigated. Cosmic fugue courage of our questions!

15 |

Decipherment. Muse about tesseract kindling the energy hidden in matter, Rig Veda are creatures of the cosmos, culture network of wormholes, a very small stage in a vast cosmic arena, a still more glorious dawn awaits. With pretty stories for which there's little good evidence, star stuff harvesting star light across the centuries dream of the mind's eye courage of our questions hundreds of thousands inconspicuous motes of rock and gas made in the interiors of collapsing stars extraplanetary, Jean-François Champollion a billion trillion. Extraordinary claims require extraordinary evidence rich in mystery, shores of the cosmic ocean how far away cosmos.

16 |

Tunguska event a still more glorious dawn awaits at the edge of forever Drake Equation are creatures of the cosmos. Dream of the mind's eye vanquish the impossible take root and flourish white dwarf birth Cambrian explosion. Across the centuries rogue permanence of the stars brain is the seed of intelligence, Rig Veda? Across the centuries with pretty stories for which there's little good evidence tingling of the spine science, dispassionate extraterrestrial observer are creatures of the cosmos permanence of the stars white dwarf, billions upon billions concept of the number one the only home we've ever known Cambrian explosion? A very small stage in a vast cosmic arena from which we spring laws of physics Sea of Tranquility something incredible is waiting to be known courage of our questions tesseract and billions upon billions upon billions upon billions upon billions upon billions upon billions?

17 |
18 | ); 19 | } 20 | } 21 | 22 | 23 | // Store 24 | /////////////////////////// 25 | 26 | const demoState = (state) => { 27 | return { font: state.font }; 28 | }; 29 | 30 | 31 | // Export 32 | /////////////////////////// 33 | 34 | export default connect(demoState)(Demo); 35 | -------------------------------------------------------------------------------- /src/components/Demo/Demo.less: -------------------------------------------------------------------------------- 1 | .Demo { 2 | box-sizing: border-box; 3 | padding: 5vw; 4 | width: 85%; 5 | max-width: 800px; 6 | margin: 0 auto; 7 | 8 | // namespaced here to only effect the switcher 9 | font-size: 20px; 10 | line-height: 1.9; 11 | color: darken(@color_text, 10%); 12 | 13 | p { 14 | margin: 1em 0; 15 | } 16 | 17 | b, i { 18 | color: darken(@color_text, 20%); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | 5 | // Data 6 | /////////////////////////// 7 | 8 | import {fonts} from '../../data/fonts.json'; 9 | 10 | 11 | // Component 12 | /////////////////////////// 13 | 14 | class Menu extends React.Component { 15 | _handleClick(e) { 16 | this.props._switchFont(e.target.value); 17 | this.props._toggleMenu(); 18 | } 19 | 20 | render() { 21 | return ( 22 | 36 | ); 37 | } 38 | } 39 | 40 | 41 | // Store 42 | /////////////////////////// 43 | 44 | const menuState = (state) => { 45 | return { font: state.font, isMenuToggled: state.isMenuToggled }; 46 | }; 47 | 48 | const menuDispatch = (dispatch) => { 49 | return { 50 | _switchFont: (font) => { 51 | dispatch({ type: 'SWITCH_FONT', font: font }); 52 | }, 53 | _toggleMenu: () => { 54 | dispatch({ type: 'TOGGLE_MENU' }); 55 | } 56 | } 57 | }; 58 | 59 | 60 | // Export 61 | /////////////////////////// 62 | 63 | export default connect(menuState, menuDispatch)(Menu); 64 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.less: -------------------------------------------------------------------------------- 1 | .Menu { 2 | box-sizing: border-box; 3 | position: absolute; 4 | left: 100%; 5 | bottom: 0; 6 | width: 275px; 7 | margin-left: 7px; 8 | background: @color_white; 9 | box-shadow: @elevation_100; 10 | border-radius: 5px; 11 | border: 1px solid @color_gray; 12 | -webkit-transition: all 200ms ease-in-out; 13 | opacity: 1; 14 | -webkit-transform: translateX(0); 15 | z-index: 100; 16 | 17 | &[hidden] { 18 | opacity: 0; 19 | pointer-events: none; 20 | -webkit-transform: translateX(-10px); 21 | } 22 | } 23 | 24 | .Menu__font { 25 | color: @color_text; 26 | border-bottom: 1px solid @color_gray; 27 | 28 | &:last-child { 29 | border-bottom: none; 30 | } 31 | 32 | &.selected { 33 | color: darken(@color_gray, 60%); 34 | } 35 | 36 | .wf-loading & { 37 | visibility: hidden; 38 | } 39 | } 40 | 41 | 42 | .Menu__font label { 43 | display: block; 44 | padding: 15px 18px; 45 | } 46 | 47 | .Menu__font input { 48 | margin-top: 7px; 49 | margin-right: 10px; 50 | vertical-align: top; 51 | } 52 | 53 | .Menu__font.selected .Menu__fontName:after { 54 | content: '20px'; 55 | margin-left: 7px; 56 | font-size: 11px; 57 | font-family: 'Helvetica Neue'; 58 | color: lighten(@color_text, 25%); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Trigger/Trigger.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | 5 | // Component 6 | /////////////////////////// 7 | 8 | class Trigger extends React.Component { 9 | _handleOnClick(e) { 10 | this.props._toggleMenu(); 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | ); 17 | } 18 | } 19 | 20 | 21 | // Store 22 | /////////////////////////// 23 | 24 | const triggerState = (state) => { 25 | return { isMenuToggled: state.isMenuToggled }; 26 | }; 27 | 28 | const triggerDispatch = (dispatch) => { 29 | return { 30 | _toggleMenu: () => { 31 | dispatch({ type: 'TOGGLE_MENU' }); 32 | } 33 | } 34 | }; 35 | 36 | 37 | // Export 38 | /////////////////////////// 39 | 40 | export default connect(triggerState, triggerDispatch)(Trigger); 41 | -------------------------------------------------------------------------------- /src/components/Trigger/Trigger.less: -------------------------------------------------------------------------------- 1 | .Trigger { 2 | cursor: pointer; 3 | padding: 10px 13px; 4 | border: none; 5 | font-size: 23px; 6 | border-radius: 5px; 7 | background: @color_blue; 8 | color: @color_white; 9 | z-index: 200; 10 | 11 | &:hover { 12 | background: darken(@color_blue, 5%); 13 | } 14 | 15 | &.toggled { 16 | background: lighten(@color_blue, 20%); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/data/fonts.json: -------------------------------------------------------------------------------- 1 | { 2 | "fonts": [ 3 | { 4 | "name": "Roboto", 5 | "css_name": "roboto", 6 | "needs_loaded": true 7 | }, 8 | { 9 | "name": "Open Sans", 10 | "css_name": "open sans", 11 | "needs_loaded": true 12 | }, 13 | { 14 | "name": "Georgia", 15 | "css_name": "georgia", 16 | "needs_loaded": false 17 | }, 18 | { 19 | "name": "Helvetica", 20 | "css_name": "helvetica neue", 21 | "needs_loaded": false 22 | }, 23 | { 24 | "name": "Avenir", 25 | "css_name": "avenir", 26 | "needs_loaded": false 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | 5 | var config = { 6 | entry: [ './src/app.js', './src/app.less', ], 7 | output: { path: './src/public/', filename: 'app.packed.js' }, 8 | module : { 9 | loaders : [ 10 | { test : /\.js?/, loader: 'babel-loader', query: { presets: ['es2015', 'react', 'stage-2'] } }, 11 | { test: /\.json$/, loader: 'json' }, 12 | { test: /\.less$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader") } 13 | ] 14 | }, 15 | plugins: [ new ExtractTextPlugin("app.packed.css", { allChunks: true }) ] 16 | }; 17 | 18 | module.exports = config; 19 | --------------------------------------------------------------------------------