From 0fe906ae7453aa684e998bbcc7a78b62d84f0206 Mon Sep 17 00:00:00 2001 From: Joris Guyonvarch Date: Sun, 6 Apr 2014 22:55:16 +0200 Subject: Show resume and projects from a configuration file --- .gitignore | 3 + .tmuxinator.yml | 8 + Makefile | 20 + PersonalPage.cabal | 76 + README.md | 42 + application.conf | 5 + build | 2 + data.yaml | 308 ++++ default.nix | 13 + public/images/icon.png | Bin 0 -> 701 bytes .../font-awesome-4.2.0/css/font-awesome.css | 1672 ++++++++++++++++++++ .../font-awesome-4.2.0/css/font-awesome.min.css | 4 + .../font-awesome-4.2.0/fonts/FontAwesome.otf | Bin 0 -> 85908 bytes .../fonts/fontawesome-webfont.eot | Bin 0 -> 56006 bytes .../fonts/fontawesome-webfont.svg | 520 ++++++ .../fonts/fontawesome-webfont.ttf | Bin 0 -> 112160 bytes .../fonts/fontawesome-webfont.woff | Bin 0 -> 65452 bytes public/stylesheets/reset.css | 57 + resume/email.png | Bin 0 -> 1457 bytes resume/externalLink.png | Bin 0 -> 957 bytes resume/git.png | Bin 0 -> 1744 bytes resume/missfont.log | 4 + resume/resume.cls | 101 ++ src/Conf.hs | 27 + src/Daemon.hs | 18 + src/Daemon/Frequency.hs | 8 + src/Date.hs | 17 + src/Design/Color.hs | 27 + src/Design/Global.hs | 66 + src/Design/Header.hs | 56 + src/Design/Media.hs | 36 + src/Design/Name.hs | 15 + src/Design/NotFound.hs | 20 + src/Design/Projects.hs | 51 + src/Design/Resume.hs | 125 ++ src/Design/Size.hs | 28 + src/Main.hs | 67 + src/Model.hs | 25 + src/Model/Company.hs | 13 + src/Model/Date.hs | 30 + src/Model/Degree.hs | 18 + src/Model/Header.hs | 25 + src/Model/Identity.hs | 14 + src/Model/Job.hs | 22 + src/Model/Project.hs | 18 + src/Model/School.hs | 13 + src/Model/SkillType.hs | 15 + src/Model/Translated.hs | 19 + src/Model/Translation/Key.hs | 28 + src/Model/Translation/Language.hs | 12 + src/Model/Translation/Message.hs | 90 ++ src/PDF.hs | 41 + src/Resume.hs | 34 + src/Utils/String.hs | 21 + src/View/Git.hs | 37 + src/View/Header.hs | 42 + src/View/Icon.hs | 17 + src/View/Interval.hs | 39 + src/View/LaTeX/Resume.hs | 148 ++ src/View/NotFound.hs | 26 + src/View/Page.hs | 33 + src/View/Project.hs | 66 + src/View/Resume.hs | 140 ++ 63 files changed, 4382 insertions(+) create mode 100644 .gitignore create mode 100644 .tmuxinator.yml create mode 100644 Makefile create mode 100644 PersonalPage.cabal create mode 100644 README.md create mode 100644 application.conf create mode 100755 build create mode 100644 data.yaml create mode 100644 default.nix create mode 100644 public/images/icon.png create mode 100644 public/stylesheets/font-awesome-4.2.0/css/font-awesome.css create mode 100644 public/stylesheets/font-awesome-4.2.0/css/font-awesome.min.css create mode 100644 public/stylesheets/font-awesome-4.2.0/fonts/FontAwesome.otf create mode 100644 public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.eot create mode 100644 public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.svg create mode 100644 public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf create mode 100644 public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.woff create mode 100644 public/stylesheets/reset.css create mode 100644 resume/email.png create mode 100644 resume/externalLink.png create mode 100644 resume/git.png create mode 100644 resume/missfont.log create mode 100644 resume/resume.cls create mode 100644 src/Conf.hs create mode 100644 src/Daemon.hs create mode 100644 src/Daemon/Frequency.hs create mode 100644 src/Date.hs create mode 100644 src/Design/Color.hs create mode 100644 src/Design/Global.hs create mode 100644 src/Design/Header.hs create mode 100644 src/Design/Media.hs create mode 100644 src/Design/Name.hs create mode 100644 src/Design/NotFound.hs create mode 100644 src/Design/Projects.hs create mode 100644 src/Design/Resume.hs create mode 100644 src/Design/Size.hs create mode 100644 src/Main.hs create mode 100644 src/Model.hs create mode 100644 src/Model/Company.hs create mode 100644 src/Model/Date.hs create mode 100644 src/Model/Degree.hs create mode 100644 src/Model/Header.hs create mode 100644 src/Model/Identity.hs create mode 100644 src/Model/Job.hs create mode 100644 src/Model/Project.hs create mode 100644 src/Model/School.hs create mode 100644 src/Model/SkillType.hs create mode 100644 src/Model/Translated.hs create mode 100644 src/Model/Translation/Key.hs create mode 100644 src/Model/Translation/Language.hs create mode 100644 src/Model/Translation/Message.hs create mode 100644 src/PDF.hs create mode 100644 src/Resume.hs create mode 100644 src/Utils/String.hs create mode 100644 src/View/Git.hs create mode 100644 src/View/Header.hs create mode 100644 src/View/Icon.hs create mode 100644 src/View/Interval.hs create mode 100644 src/View/LaTeX/Resume.hs create mode 100644 src/View/NotFound.hs create mode 100644 src/View/Page.hs create mode 100644 src/View/Project.hs create mode 100644 src/View/Resume.hs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c0f050 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist-newstyle +public/resumes +.ghc.environment.* diff --git a/.tmuxinator.yml b/.tmuxinator.yml new file mode 100644 index 0000000..da872d4 --- /dev/null +++ b/.tmuxinator.yml @@ -0,0 +1,8 @@ +name: personalPage +startup_window: app + +windows: + - console: + - clear + - app: + - make clean watch diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e350236 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +all: build + +start: + @nix-shell --command "tmuxinator local" + +stop: + @nix-shell --command "tmux kill-session -t personalPage" + +clean: + @cabal new-clean > /dev/null + +watch: + @nodemon --watch src/ application.conf ./data.yaml resume/ --ext hs,conf,yaml,cls --exec 'clear && make build-and-launch-server --silent' + +build-and-launch-server: + @(pkill personalPage || true) && (cabal new-run || true) + +.PHONY: build +build: + @cabal new-build || true diff --git a/PersonalPage.cabal b/PersonalPage.cabal new file mode 100644 index 0000000..d9386eb --- /dev/null +++ b/PersonalPage.cabal @@ -0,0 +1,76 @@ +Name: PersonalPage +Version: 1.0 +Synopsis: My personal page +Homepage: guyonvarch.me +License: GPL-3 +Author: Joris Guyonvarch +Category: Web +Build-type: Simple +Cabal-version: >= 1.8 + +Executable personalPage + Hs-source-dirs: src + Main-is: Main.hs + Ghc-options: -Wall -Werror + + Build-depends: + base + , text + , aeson + , yaml + , blaze-html + , blaze-markup + , clay + , bytestring + , HaTeX + , process + , mtl + , transformers + , temporary + , scotty + , wai-middleware-static + , time + , config-manager + , lens + , directory + , filepath + + Other-modules: + Conf + , Daemon + , Daemon.Frequency + , Date + , Design.Color + , Design.Global + , Design.Header + , Design.Media + , Design.NotFound + , Design.Projects + , Design.Resume + , Design.Size + , Model + , Model.Company + , Model.Date + , Model.Degree + , Model.Header + , Model.Identity + , Model.Job + , Model.Project + , Model.School + , Model.SkillType + , Model.Translated + , Model.Translation.Key + , Model.Translation.Language + , Model.Translation.Message + , PDF + , Resume + , Utils.String + , View.Git + , View.Header + , View.Icon + , View.Interval + , View.LaTeX.Resume + , View.NotFound + , View.Page + , View.Project + , View.Resume diff --git a/README.md b/README.md new file mode 100644 index 0000000..7190ea6 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Personal Page + +Generate a website presenting resume and project from data given by a yaml +file. + +## Required dependency + +- `pdflatex` + +## Getting started + +Install nix: + +``` bash +curl https://nixos.org/nix/install | sh +``` + +Then, launch the environment with: + +``` bash +make start +``` + +Later, stop the environment with: + +```bash +make stop +``` + +## Build + +``` sh +make build +``` + +## Configuration + +``` +port = 3000 +git = "https://gitlab.com/" +generateResumes = Daily # Hourly / Daily +``` diff --git a/application.conf b/application.conf new file mode 100644 index 0000000..8a2979e --- /dev/null +++ b/application.conf @@ -0,0 +1,5 @@ +port = 3000 +git = "https://gitlab.com/" +generateResumes = Daily + +importMaybe "local.conf" diff --git a/build b/build new file mode 100755 index 0000000..9eef0d2 --- /dev/null +++ b/build @@ -0,0 +1,2 @@ +#!/bin/sh +nix-shell --command "make clean build" diff --git a/data.yaml b/data.yaml new file mode 100644 index 0000000..27fae8d --- /dev/null +++ b/data.yaml @@ -0,0 +1,308 @@ +description: I’m a developer using functional programming. View my resume and projects. + +identity: + name: Joris Guyonvarch + website: https://guyonvarch.me + git: guyonvarch + +jobs: + - name: + english: Software Architect + french: Architecte Logiciel + description: + english: Web Oriented Architecture (WOA) for information systems. + french: Architecture web pour les systèmes d’information. + company: + name: Zengularity + location: Paris + beginDate: + month: 03 + year: 2014 + details: + - english: Training platform for the unemployed in Seine-Saint-Denis. + french: Catalogue de formations pour les bénéficiaires du RSA en Seine-Saint-Denis. + - english: File house insurance claims for Saretec. + french: Déclaration de sinistres pour Saretec. + - english: Unit and automatic franking of Colissimo labels for La Poste. + french: Affranchissement à l’unité et automatique d’étiquettes Colissimo pour La Poste. + - english: Participant’s pages for the Mutuelle Nationale Territoriale (MNT). + french: Espace adhérent de la Mutuelle Nationale Territoriale (MNT). + - english: Food sample analysis with results visualization for Adisseo. + french: Analyse d’échantillons alimentaires avec visualisation des résultats pour Adisseo. + technos: + - Scala + - Scala.js + - Elm + - Akka + - Play! + - MongoDB + - PostgreSQL + - ElasticSearch + - name: + english: Phd Intern + french: Chercheur stagiaire + description: + english: Scalable query-based faceted search for guided and expressive + search on the semantic web. + french: Mise à l’échelle d’une recherche à facettes, à base de requêtes + guidées et expressives, dans le web sémantique. + company: + name: IRISA + location: Rennes + beginDate: + month: 02 + year: 2013 + endDate: + month: 06 + year: 2013 + technos: + - Semantic Web + - Scala + - Java + - LaTeX + - GWT + - Jena + - name: + english: R&D Engineering Intern + french: Ingénieur de recherche stagiaire + description: + english: Guided querying and edition of semantic web information. + french: Recherche et édition guidées de données du web sémantique. + company: + name: Mission Critical IT + location: Bruxelles + beginDate: + month: 06 + year: 2012 + endDate: + month: 08 + year: 2012 + technos: + - Semantic Web + - Mercury + +degrees: + - name: + english: Master of Science Computer Science + french: Diplôme d’ingénieur en informatique + school: + name: INSA + location: Rennes + year: 2013 + topics: + - english: Language + french: Langage + - english: System + french: Système + - english: Network + french: Réseau + - name: + english: Master of Research Computer Science + french: Master Recherche en Informatique + school: + name: University of Rennes I + year: 2013 + topics: + - english: Machine learning + french: Apprentissage artificiel + - english: Modelisation + french: Modélisation + - english: Data indexation + french: Indexation de données + +skillTypes: + - name: + english: Leader + french: Meneur + skills: + - english: Create tasks and subtasks from functional needs. + french: Rédiger des tâches et sous-tâches à partir des besoins. + - english: Optimise task order for the development team. + french: Optimiser l’ordre des tâches pour l’équipe de développement. + - name: + english: Developer + french: Développeur + skills: + - english: Program with functional languages (Haskell, Scala, Elm). + french: Programmer avec des langages fonctionnels (Haskell, Scala, Elm). + - english: Build web applications (HTTP protocol; HTML, CSS, Javascript and JSON languages). + french: Construire des applications web (protocole HTTP ; langages HTML, CSS, Javascript et JSON). + - english: Interact with data (SQL, MongoDB, SPARQL). + french: Interagir avec des données (SQL, MongoDB, SPARQL). + - english: Work with versioning (Git). + french: Travailler avec des versions (Git). + - english: Check code and run of developments. + french: Vérifier techniquement et fonctionnellement les développements. + - name: + english: Researcher + french: Chercheur + skills: + - english: Conduct a bibliographic research. + french: Mener une recherche bibliographique. + - english: Build a prototype and experiment from it. + french: Construire un prototype et expérimenter à partir de ce prototype. + - english: Analyse the results from an experimentation and check the hypotheses. + french: Analyser les résultats du prototype et vérifier les hypothèses. + - english: Present a research study through an article and a slideshow (LaTeX). + french: Présenter un résultat de recherche à travers un article et un + diaporama (LaTeX). + +interests: + - english: Tune my workstation (NixOS, i3, bépo keyboard layout). + french: Ajuster mon poste informatique (NixOS, i3, clavier bépo). + - english: Create video games (SDL, Elm). + french: Créer des jeux vidéos (SDL, Elm). + +projects: + - name: Shared Cost + git: guyonvarch/sharedCost + technologies: + - Haskell + - Scotty + - Elm + - Clay + - Persistent + description: + english: + Share costs with a group of people with a Rich Internet Application + (RIA). + french: + Partage des coûts au sein d’un groupe de personnes avec une Application + Internet Riche (RIA). + - name: ad-listener + git: guyonvarch/ad-listener + technologies: + - Haskell + - TagSoup + description: + english: + Watch for new ads according to custom search criteria and send an email + notification each time a new ad is available. + french: + Surveille l’ajout de nouvelles annonces à partir de critères de + recherche personnalisés et notifie par courriel lorsqu’une nouvelle + annonce est disponible. + - name: Reading + git: guyonvarch/reading + pageLink: http://guyonvarch.gitlab.io/reading + technologies: + - Scala + - Scala.js + - ScalaCSS + - Scalatags + - Scala.Rx + description: + english: Guide students to find a book by refining the result set with + faceted search. + french: Guide les élèves à trouver un livre en raffinant le résultat avec + une recherche à facettes. + - name: Map Marks + git: guyonvarch/mapMarks + pageLink: http://guyonvarch.gitlab.io/mapMarks + technologies: + - JavaScript + - Leaflet + description: + english: Show marks on a map from any given CSV file. + french: Affiche des marqueurs sur une carte à partir d’un fichier CSV. + - name: cAtchVoid + git: guyonvarch/catchvoid + pageLink: http://guyonvarch.gitlab.io/catchvoid + technologies: + - Elm + description: + english: + Catch the points of your color, avoid the others. You can switch colors + in order to reverse the game mechanic. Get the best score. + french: + Attrape les points de ta couleur tout en évitant les autres points. Tu + peux inverser la couleur de ton joueur, ce qui a pour effet d’inverser + la mécanique du jeu. Fais le meilleur score possible. + - name: Personal Page + git: guyonvarch/personalPage + pageLink: https://guyonvarch.me/ + technologies: + - Haskell + - Scotty + - BlazeHtml + - Clay + - HaTeX + description: + english: + Show resume and projects from YAML data with a web application. + french: + Présente le curriculum et les projets à partir de données au format + YAML avec une application web. + - name: Timer + git: guyonvarch/timer + pageLink: http://guyonvarch.gitlab.io/timer + technologies: + - Elm + description: + english: + Create and manage timers with a Rich Internet Application (RIA). + french: + Crée et gère une liste de minuteurs avec une Application Internet Riche + (RIA). + - name: Cooking + git: guyonvarch/cooking + pageLink: https://guyonvarch.gitlab.io/cooking + technologies: + - Haskell + - Hakyll + - Clay + description: + english: + Show recipes as a blog from markdown files. + french: + Présente des recettes sous la forme d’un blog à partir de fichiers markdown. + - name: nixos-config + git: guyonvarch/nixos-config + technologies: + - Nix + description: + english: + Declarative configuration of my NixOS system. + french: + Configuration déclarative de mon système NixOS. + - name: Events + git: guyonvarch/events + technologies: + - Haskell + - Parsec + description: + english: + Notify by email if there are any event today or next week. + french: + Notifie par courriel s’il y a des événements aujourd’hui ou la semaine + prochaine. + - name: Shopping + git: guyonvarch/shopping + technologies: + - Haskell + - Scotty + - Elm + - Clay + - Persistent + description: + english: + Creation, management and printing of shopping lists with a Rich + Internet Application (RIA). + french: + Création, gestion et impressions de listes de courses avec une + Application Internet Riche (RIA). + - name: Makeup + git: guyonvarch/makeup + technologies: + - HTML + - CSS + - JavaScript + - Markdown + description: + english: + Present makup tips grouped by categories thanks to other makeup + websites that are referenced. Makeup’s content is written in Markdown. + french: + Présente des astuces de maquillage groupées par catégories à partir de + nombreux sites de maquillage cités en référence. Le contenu de Makeup + est écrit en Markdown. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..155cee9 --- /dev/null +++ b/default.nix @@ -0,0 +1,13 @@ +with import {}; { + env = stdenv.mkDerivation { + name = "env"; + buildInputs = [ + cabal-install + nodePackages.nodemon + nodejs-8_x + tmux + tmuxinator + zlib + ]; + }; +} diff --git a/public/images/icon.png b/public/images/icon.png new file mode 100644 index 0000000..115a297 Binary files /dev/null and b/public/images/icon.png differ diff --git a/public/stylesheets/font-awesome-4.2.0/css/font-awesome.css b/public/stylesheets/font-awesome-4.2.0/css/font-awesome.css new file mode 100644 index 0000000..4040b3c --- /dev/null +++ b/public/stylesheets/font-awesome-4.2.0/css/font-awesome.css @@ -0,0 +1,1672 @@ +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.2.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} diff --git a/public/stylesheets/font-awesome-4.2.0/css/font-awesome.min.css b/public/stylesheets/font-awesome-4.2.0/css/font-awesome.min.css new file mode 100644 index 0000000..ec53d4d --- /dev/null +++ b/public/stylesheets/font-awesome-4.2.0/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} \ No newline at end of file diff --git a/public/stylesheets/font-awesome-4.2.0/fonts/FontAwesome.otf b/public/stylesheets/font-awesome-4.2.0/fonts/FontAwesome.otf new file mode 100644 index 0000000..81c9ad9 Binary files /dev/null and b/public/stylesheets/font-awesome-4.2.0/fonts/FontAwesome.otf differ diff --git a/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.eot b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..84677bc Binary files /dev/null and b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.eot differ diff --git a/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.svg b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..d907b25 --- /dev/null +++ b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..96a3639 Binary files /dev/null and b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf differ diff --git a/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.woff b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..628b6a5 Binary files /dev/null and b/public/stylesheets/font-awesome-4.2.0/fonts/fontawesome-webfont.woff differ diff --git a/public/stylesheets/reset.css b/public/stylesheets/reset.css new file mode 100644 index 0000000..ad3a3cc --- /dev/null +++ b/public/stylesheets/reset.css @@ -0,0 +1,57 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +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, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* Box Sizing */ +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} diff --git a/resume/email.png b/resume/email.png new file mode 100644 index 0000000..3f50cc3 Binary files /dev/null and b/resume/email.png differ diff --git a/resume/externalLink.png b/resume/externalLink.png new file mode 100644 index 0000000..cf25c68 Binary files /dev/null and b/resume/externalLink.png differ diff --git a/resume/git.png b/resume/git.png new file mode 100644 index 0000000..96c2653 Binary files /dev/null and b/resume/git.png differ diff --git a/resume/missfont.log b/resume/missfont.log new file mode 100644 index 0000000..45e2b6b --- /dev/null +++ b/resume/missfont.log @@ -0,0 +1,4 @@ +mktextfm pagk8t +mktextfm pagk8t +mktextfm pagk8t +mktextfm pagk8t diff --git a/resume/resume.cls b/resume/resume.cls new file mode 100644 index 0000000..e9fc2e6 --- /dev/null +++ b/resume/resume.cls @@ -0,0 +1,101 @@ +\NeedsTeXFormat{LaTeX2e} +\ProvidesClass{joris-cv}[2015/11/27 Joris CV class] +\LoadClass[10pt]{article} + +\RequirePackage[hmargin=3cm,tmargin=2cm,bmargin=3.5cm,footskip=2cm]{geometry} + +\RequirePackage[utf8]{inputenc} +\RequirePackage[T1]{fontenc} +\RequirePackage{fp,etoolbox,marvosym,bookman,titlesec,hyperref,tabularx,graphicx,fancyhdr,changepage,lastpage,ulem} +\RequirePackage[dvipsnames]{xcolor} +\RequirePackage[inline]{enumitem} +\RequirePackage{xifthen} + +\setlength{\parindent}{0pt} +\color[HTML]{303030} % Default foreground color +\definecolor{link}{HTML}{505070} +\hypersetup{colorlinks,breaklinks,urlcolor=link,linkcolor=link} +\setlength{\tabcolsep}{0pt} + +\pagestyle{fancy} +\lhead{} +\chead{} +\rhead{} +\renewcommand{\headrulewidth}{0pt} +\cfoot{\thepage\ / \pageref{LastPage}} + +\newcommand{\header}[5] { + \begin{minipage}[t]{0.65\textwidth} + \centering + {\Huge #1} \\ + \vspace{0.6em} + {\Large #2} \\ + \end{minipage} + \begin{minipage}[t]{0.35\textwidth} + \begin{flushright} + \vspace{-1.3em} + \personal{#3}{#4}{#5} + \end{flushright} + \end{minipage} + \vspace{1em} +} + +\newcommand{\personal}[3] { + \begin{tabular}{p{15pt}l} + \iconLink{externalLink.png}{#1}{#1} + \iconLink{git.png}{#2#3}{#3} + \end{tabular} +} + +\newcommand{\iconLink}[3] { + \includegraphics[width=8px]{#1} & \href{#2}{\sf\small#3} \vspace{0.3em} \\ +} + +\titleformat{\section} + {\LARGE\scshape\raggedright\color{brown}} % format + {} % label + {0em} % sep + {} % before code + [\titlerule\vspace{1em}] % after + +\titleformat + {\subsection} % command + {\large\raggedright\bf} % format + {} % label + {0em} % sep + {} % before code + [\vspace{0.5em}] % after + +\newcommand{\position}[6] { + {\large\bf #1} \hspace{0.2em} {\color{gray}#2} \vspace{0.5em} + + {\color{OliveGreen}#3} \vspace{0.5em} + + \ifthenelse{\isempty{#4}}{}{#4 \vspace{1em}} + + \ifthenelse{\isempty{#5}}{}{#5 \vspace{1em}} + + \ifthenelse{\isempty{#6}}{}{#6 \vspace{1em}} + + \vspace{0.8em} +} + +\newenvironment{bullets} { + \vspace{-0.5em} + \begin{enumerate}[label=\color{brown}$\bullet$,leftmargin=0.5cm]\itemsep2pt +} { + \end{enumerate} + \vspace{-0.5em} +} + +\newenvironment{technos} { + \begin{enumerate*}[label={}] +} { + \end{enumerate*} +} + +\newcommand{\techno}[1] { + \hspace{-0.2cm} + {\color{gray}\uline{\textcolor{black}{#1}}} + \hspace{0.1cm} +} diff --git a/src/Conf.hs b/src/Conf.hs new file mode 100644 index 0000000..cd00fd9 --- /dev/null +++ b/src/Conf.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Conf + ( getConf + , Conf(..) + ) where + +import Data.Text (Text) +import qualified Data.ConfigManager as Conf + +import Daemon.Frequency (Frequency) + +data Conf = Conf + { port :: Int + , git :: String + , generateResumes :: Frequency + } deriving (Read, Eq, Show) + +getConf :: FilePath -> IO (Either Text Conf) +getConf path = + (flip fmap) (Conf.readConfig path) (\configOrError -> do + conf <- configOrError + Conf <$> + Conf.lookup "port" conf <*> + Conf.lookup "git" conf <*> + Conf.lookup "generateResumes" conf + ) diff --git a/src/Daemon.hs b/src/Daemon.hs new file mode 100644 index 0000000..1351ad5 --- /dev/null +++ b/src/Daemon.hs @@ -0,0 +1,18 @@ +module Daemon + ( runDaemon + ) where + +import Control.Concurrent (threadDelay) +import Control.Monad (forever) + +import Daemon.Frequency + +runDaemon :: Frequency -> (() -> IO ()) -> IO () +runDaemon frequency process = + forever $ do + process () + threadDelay (sleepDelayMs frequency) + +sleepDelayMs :: Frequency -> Int +sleepDelayMs Hourly = 1000000 * 60 * 60 +sleepDelayMs Daily = (sleepDelayMs Hourly) * 24 diff --git a/src/Daemon/Frequency.hs b/src/Daemon/Frequency.hs new file mode 100644 index 0000000..d989d7f --- /dev/null +++ b/src/Daemon/Frequency.hs @@ -0,0 +1,8 @@ +module Daemon.Frequency + ( Frequency(..) + ) where + +data Frequency = + Hourly + | Daily + deriving (Eq, Read, Show) diff --git a/src/Date.hs b/src/Date.hs new file mode 100644 index 0000000..e3c16e5 --- /dev/null +++ b/src/Date.hs @@ -0,0 +1,17 @@ +module Date + ( getCurrentDate + ) where + +import Data.Time.Clock +import Data.Time.Calendar +import Data.Time.LocalTime + +import Model.Date + +getCurrentDate :: IO Date +getCurrentDate = do + now <- getCurrentTime + timezone <- getCurrentTimeZone + let zoneNow = utcToLocalTime timezone now + let (y, m, _) = toGregorian $ localDay zoneNow + return (Date m (fromIntegral y)) diff --git a/src/Design/Color.hs b/src/Design/Color.hs new file mode 100644 index 0000000..bbe20c4 --- /dev/null +++ b/src/Design/Color.hs @@ -0,0 +1,27 @@ +module Design.Color where + +import qualified Clay.Color as C + +white :: C.Color +white = C.white + +red :: C.Color +red = C.rgb 170 57 57 + +orange :: C.Color +orange = C.rgb 182 119 25 + +green :: C.Color +green = C.rgb 0 93 0 + +blue :: C.Color +blue = C.rgb 79 182 187 + +black :: C.Color +black = C.rgb 0 0 0 + +link :: C.Color +link = blue C.-. 70 + +gray :: C.Color +gray = C.rgb 100 100 100 diff --git a/src/Design/Global.hs b/src/Design/Global.hs new file mode 100644 index 0000000..379d612 --- /dev/null +++ b/src/Design/Global.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Design.Global + ( compactDesign + ) where + +import qualified Data.Text.Lazy as T +import Data.Monoid ((<>)) + +import Clay + +import qualified Design.Color as Color +import qualified Design.Media as Media +import Design.Header (headerCss) +import Design.Resume (resumeCss) +import Design.Projects (projectsCss) +import Design.NotFound (notFoundCss) + +compactDesign :: T.Text +compactDesign = renderWith compact [] $ global + +global :: Css +global = + body ? do + color Color.black + marginBottom (px 40) + Media.mobile $ fontSize (px 16) + Media.tabletDesktop $ fontSize (px 18) + + h1 ? do + fontFamily [] [monospace] + fontWeight bold + color Color.red + + Media.mobile $ do + lineHeight (px 30) + fontSize (px 22) + marginBottom (px 20) + marginTop (px 35) + + Media.tablet $ do + lineHeight (px 40) + fontSize (px 27) + marginBottom (px 35) + marginTop (px 45) + + Media.desktop $ do + lineHeight (px 50) + fontSize (px 30) + marginBottom (px 40) + marginTop (px 55) + + a ? do + textDecoration none + color Color.link + transition "color" (sec 0.3) easeOut (sec 0) + focus & outline solid (px 0) Color.white + + (a # hover) <> (a # focus) ? do + textDecoration underline + color Color.blue + + headerCss + resumeCss + projectsCss + notFoundCss diff --git a/src/Design/Header.hs b/src/Design/Header.hs new file mode 100644 index 0000000..d2bbace --- /dev/null +++ b/src/Design/Header.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Design.Header + ( headerCss + ) where + +import Data.Monoid ((<>)) + +import Clay +import Clay.Display (displayTable) + +import qualified Design.Color as Color +import qualified Design.Media as Media + +headerCss :: Css +headerCss = do + ".header" ? do + backgroundColor Color.red + color Color.white + fontSize (px 28) + + ul ? do + width (pct 100) + display displayTable + "table-layout" -: "fixed" + + li ? do + display tableCell + + a ? do + display block + height (em 3) + lineHeight (em 3) + textDecoration none + padding (px 0) (px 0) (px 0) (px 0) + textAlign (alignSide sideCenter) + color Color.white + textTransform capitalize + transition "background-color" (ms 500) ease (sec 0) + + i ? marginRight (em 0.5) + + Media.mobile $ do + i ? display none + fontSize (em 0.6) + + Media.tablet $ fontSize (em 0.8) + + (a # hover <> a # focus <> a # ".currentHeader") ? do + backgroundColor Color.red + borderBottomStyle solid + borderBottomColor (Color.red +. 40) + + Media.mobile $ borderBottomWidth (px 6) + Media.tablet $ borderBottomWidth (px 8) + Media.desktop $ borderBottomWidth (px 10) diff --git a/src/Design/Media.hs b/src/Design/Media.hs new file mode 100644 index 0000000..77220ee --- /dev/null +++ b/src/Design/Media.hs @@ -0,0 +1,36 @@ +module Design.Media + ( mobile + , mobileTablet + , tablet + , tabletDesktop + , desktop + ) where + +import Clay hiding (query) +import qualified Clay +import Clay.Stylesheet (Feature) +import qualified Clay.Media as Media + +mobile :: Css -> Css +mobile = query [Media.maxWidth mobileTabletLimit] + +mobileTablet :: Css -> Css +mobileTablet = query [Media.maxWidth tabletDesktopLimit] + +tablet :: Css -> Css +tablet = query [Media.minWidth mobileTabletLimit, Media.maxWidth tabletDesktopLimit] + +tabletDesktop :: Css -> Css +tabletDesktop = query [Media.minWidth mobileTabletLimit] + +desktop :: Css -> Css +desktop = query [Media.minWidth tabletDesktopLimit] + +query :: [Feature] -> Css -> Css +query = Clay.query Media.screen + +mobileTabletLimit :: Size LengthUnit +mobileTabletLimit = (px 520) + +tabletDesktopLimit :: Size LengthUnit +tabletDesktopLimit = (px 950) diff --git a/src/Design/Name.hs b/src/Design/Name.hs new file mode 100644 index 0000000..84c91fb --- /dev/null +++ b/src/Design/Name.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Design.Name + ( nameCss + ) where + +import Clay + +nameCss :: Css +nameCss = do + fontWeight bold + letterSpacing (px 10) + margin (px 100) (px 0) (px 80) (px 0) + lineHeight (em 1.2) + fontSize (px 48) diff --git a/src/Design/NotFound.hs b/src/Design/NotFound.hs new file mode 100644 index 0000000..ee8a0af --- /dev/null +++ b/src/Design/NotFound.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Design.NotFound + ( notFoundCss + ) where + +import Clay + +notFoundCss :: Css +notFoundCss = + + ".notFoundPage" ? do + + h1 ? do + fontSize (px 40) + fontWeight bold + margin (px 20) (px 20) (px 20) (px 20) + + p ? + margin (px 20) (px 20) (px 20) (px 20) diff --git a/src/Design/Projects.hs b/src/Design/Projects.hs new file mode 100644 index 0000000..311b7f8 --- /dev/null +++ b/src/Design/Projects.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Design.Projects + ( projectsCss + ) where + +import Prelude hiding ((**)) + +import Data.Monoid ((<>)) + +import Clay +import qualified Clay.Flexbox as CF + +import qualified Design.Color as Color +import qualified Design.Media as Media +import qualified Design.Size as Size + +projectsCss :: Css +projectsCss = + ".project" ? do + margin (pct 0) (pct 10) (pct 0) (pct 10) + + h1 ? ".separator" ? color Color.black + + ".body" ? do + Size.indentation + + ".technologies" <> ".pageLink" ? do + i ? marginRight (em 0.5) + Size.lineHeight + marginBottom (px 10) + + ".technologies" ? do + Media.mobile $ fontSize (pct 90) + ul ? do + display flex + flexWrap CF.wrap + li ? do + backgroundColor Color.orange + color Color.white + borderRadius (px 2) (px 2) (px 2) (px 2) + margin (px 0) (px 10) (px 5) (px 0) + ":last-child:after" & marginRight (px 0) + + Media.desktop $ padding (px 0) (px 10) (px 0) (px 10) + Media.mobileTablet $ padding (px 0) (px 5) (px 0) (px 5) + + ".description" ? do + Size.lineHeight + marginTop (px 10) + Media.desktop $ width (pct 50) diff --git a/src/Design/Resume.hs b/src/Design/Resume.hs new file mode 100644 index 0000000..d668987 --- /dev/null +++ b/src/Design/Resume.hs @@ -0,0 +1,125 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Design.Resume + ( resumeCss + ) where + +import Prelude hiding ((**)) + +import Clay +import qualified Clay.Flexbox as CF + +import qualified Design.Color as Color +import qualified Design.Media as Media +import qualified Design.Size as Size + +resumeCss :: Css +resumeCss = + ".section" ? do + position relative + margin (pct 0) (pct 10) (pct 0) (pct 10) + + h1 ? textTransform capitalize + + ".identity" ? do + ".mail" <> ".git" ? do + Size.indentation + i ? marginRight (em 0.5) + + Media.mobile $ do + fontSize (px 14) + marginBottom (px 15) + + Media.tablet $ do + fontSize (px 18) + marginBottom (px 25) + + Media.desktop $ do + fontSize (px 20) + marginBottom (px 30) + + ".pdf" ? do + position absolute + right (px 0) + top (px 0) + color Color.red + transition "all" (ms 100) ease (sec 0) + i ? marginRight (px 0) + hover & transform (scale 1.2 1.2) + + Media.mobile $ do + lineHeight (px 30) + height (px 30) + fontSize (px 20) + + Media.tablet $ do + lineHeight (px 40) + height (px 40) + fontSize (px 30) + + Media.desktop $ do + lineHeight (px 50) + height (px 50) + fontSize (px 40) + + ".item" ? do + marginBottom (px 40) + Size.indentation + Media.mobile $ marginBottom (px 25) + + ".title" <> ".location" <> ".description" ? do + Size.lineHeight + + ".title" ? do + Media.desktop $ do + display flex + marginBottom (px 10) + + ".skills" & do + Size.tabletMarginBottom + + ".text" ? do + backgroundColor Color.orange + color Color.white + padding (px 0) (px 10) (px 0) (px 10) + sym borderRadius (px 2) + Media.mobileTablet $ marginBottom (px 10) + + ".date" ? do + fontStyle italic + Media.mobile $ fontSize (pct 90) + Media.desktop $ marginLeft (px 15) + + ".description" ? ".detail" ? + marginTop Size.listItemSep + + ".location" ? do + color Color.green + Media.mobile $ do + fontSize (pct 90) + marginBottom (px 10) + Size.tabletMarginBottom + + ".itemList" ? marginTop (px 5) + + ".bullets" |> ".detail" ? do + Media.mobile $ marginBottom Size.listItemSep + Size.tabletMarginBottom + + ".bullets" |> li ? do + Size.lineHeight + before & do + content (stringContent "•") + color Color.red + display inlineBlock + marginRight (px 10) + + ".technos" ? do + display flex + flexWrap CF.wrap + sym2 margin (px 5) (px 0) + + ".technos" |> ".techno" ? do + lineHeight normal + borderBottom solid (px 2) lightgray + margin (px 10) (px 15) (px 5) (px 0) diff --git a/src/Design/Size.hs b/src/Design/Size.hs new file mode 100644 index 0000000..8b323bf --- /dev/null +++ b/src/Design/Size.hs @@ -0,0 +1,28 @@ +module Design.Size + ( indentation + , lineHeight + , listItemSep + , tabletMarginBottom + ) where + +import Clay hiding (lineHeight) +import qualified Clay + +import qualified Design.Media as Media + +indentation :: Css +indentation = do + Media.tablet $ marginLeft (px 10) + Media.desktop $ marginLeft (px 20) + +lineHeight :: Css +lineHeight = do + Media.mobile $ Clay.lineHeight (px 30) + Media.tablet $ Clay.lineHeight (px 35) + Media.desktop $ Clay.lineHeight (px 40) + +listItemSep :: Size LengthUnit +listItemSep = px 8 + +tabletMarginBottom :: Css +tabletMarginBottom = Media.tablet $ marginBottom (px 15) diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..fd1e076 --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Main + ( main + ) where + +import Control.Concurrent (forkIO) +import Control.Monad.IO.Class (liftIO) + +import Network.Wai.Middleware.Static + +import Web.Scotty + +import Data.Text.Lazy (isPrefixOf) +import Data.Yaml (decodeFileEither) + +import Model +import Model.Translation.Language + +import View.NotFound (renderNotFound) +import View.Page (renderPage) +import View.Project (renderProjects) +import View.Resume (renderResume) + +import Design.Global (compactDesign) + +import qualified Conf as Conf + +import Date (getCurrentDate) + +import Daemon (runDaemon) + +import Resume (generateResumes) + +main :: IO () +main = do + modelOrError <- decodeFileEither "data.yaml" + confOrError <- Conf.getConf "application.conf" + case (modelOrError, confOrError) of + (Left modelError, _) -> + putStrLn $ "Model error: " ++ (show modelError) + (_, Left confError) -> + putStrLn $ "Configuration error: " ++ (show confError) + (Right model, Right conf) -> do + _ <- forkIO . runDaemon (Conf.generateResumes conf) $ \() -> generateResumes model conf + scotty (Conf.port conf) $ do + middleware $ staticPolicy (noDots >-> addBase "public") + get "/design" $ do + addHeader "Content-Type" "text/css" + text compactDesign + get "/" $ do + language <- getLanguage + currentDate <- liftIO getCurrentDate + html $ renderPage model (renderResume conf language currentDate model) + get "/projects" $ do + language <- getLanguage + html $ renderPage model (renderProjects conf language (projects model)) + notFound $ do + language <- getLanguage + html $ renderPage model (renderNotFound language) + +getLanguage :: ActionM Language +getLanguage = do + mbLang <- header "Accept-Language" + case mbLang of + Just lang | "fr" `isPrefixOf` lang -> return French + _ -> return English diff --git a/src/Model.hs b/src/Model.hs new file mode 100644 index 0000000..846d98a --- /dev/null +++ b/src/Model.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model where + +import GHC.Generics +import Data.Yaml + +import Model.Identity (Identity) +import Model.Job (Job) +import Model.Degree (Degree) +import Model.SkillType (SkillType) +import Model.Project (Project) +import Model.Translated (Translated) + +data Model = Model + { description :: String + , identity :: Identity + , jobs :: [Job] + , degrees :: [Degree] + , skillTypes :: [SkillType] + , interests :: [Translated] + , projects :: [Project] + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Model diff --git a/src/Model/Company.hs b/src/Model/Company.hs new file mode 100644 index 0000000..3069488 --- /dev/null +++ b/src/Model/Company.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Company where + +import GHC.Generics +import Data.Yaml + +data Company = Company + { name :: String + , location :: String + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Company diff --git a/src/Model/Date.hs b/src/Model/Date.hs new file mode 100644 index 0000000..7edb6fb --- /dev/null +++ b/src/Model/Date.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Date where + +import GHC.Generics +import Data.Yaml + +data Date = Date + { month :: Int + , year :: Int + } deriving (Show, Read, Eq, Generic) + +yearAndMonthDiff :: Date -> Date -> (Int, Int) +yearAndMonthDiff d1 d2 = + let totalMonths = monthDiff d1 d2 + in (totalMonths `div` 12, totalMonths `mod` 12) + +monthDiff :: Date -> Date -> Int +monthDiff (Date m1 y1) (Date m2 y2) = + if y1 == y2 then + 1 + abs (m1 - m2) + else + let (minM, minY, maxM, maxY) = + if y1 < y2 then + (m1, y1, m2, y2) + else + (m2, y2, m1, y1) + in 12 * (maxY - minY - 1) + (13 - minM) + maxM + +instance FromJSON Date diff --git a/src/Model/Degree.hs b/src/Model/Degree.hs new file mode 100644 index 0000000..90feacc --- /dev/null +++ b/src/Model/Degree.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Degree where + +import GHC.Generics +import Data.Yaml + +import Model.School +import Model.Translated + +data Degree = Degree + { name :: Translated + , school :: School + , year :: Int + , topics :: [Translated] + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Degree diff --git a/src/Model/Header.hs b/src/Model/Header.hs new file mode 100644 index 0000000..7581fc5 --- /dev/null +++ b/src/Model/Header.hs @@ -0,0 +1,25 @@ +module Model.Header + ( Header(..) + , allHeaders + , headerLink + , headerKey + ) where + +import Model.Translation.Key (Key) +import qualified Model.Translation.Key as K + +data Header = + Resume + | Projects + deriving (Enum, Bounded, Show, Eq) + +allHeaders :: [Header] +allHeaders = [minBound..] + +headerLink :: Header -> String +headerLink Resume = "/" +headerLink Projects = "/projects" + +headerKey :: Header -> Key +headerKey Resume = K.Resume +headerKey Projects = K.Projects diff --git a/src/Model/Identity.hs b/src/Model/Identity.hs new file mode 100644 index 0000000..fb640fa --- /dev/null +++ b/src/Model/Identity.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Identity where + +import Data.Yaml +import GHC.Generics + +data Identity = Identity + { name :: String + , website :: String + , git :: String + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Identity diff --git a/src/Model/Job.hs b/src/Model/Job.hs new file mode 100644 index 0000000..f6865f4 --- /dev/null +++ b/src/Model/Job.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Job where + +import Data.Yaml +import GHC.Generics + +import Model.Company +import Model.Date +import Model.Translated + +data Job = Job + { name :: Translated + , description :: Translated + , details :: Maybe [Translated] + , technos :: Maybe [String] + , company :: Company + , beginDate :: Date + , endDate :: Maybe Date + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Job diff --git a/src/Model/Project.hs b/src/Model/Project.hs new file mode 100644 index 0000000..7825e60 --- /dev/null +++ b/src/Model/Project.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Project where + +import GHC.Generics +import Data.Yaml + +import Model.Translated + +data Project = Project + { name :: String + , technologies :: [String] + , description :: Maybe Translated + , git :: String + , pageLink :: Maybe String + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Project diff --git a/src/Model/School.hs b/src/Model/School.hs new file mode 100644 index 0000000..cb10ed9 --- /dev/null +++ b/src/Model/School.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.School where + +import GHC.Generics +import Data.Yaml + +data School = School + { name :: String + , location :: Maybe String + } deriving (Show, Read, Eq, Generic) + +instance FromJSON School diff --git a/src/Model/SkillType.hs b/src/Model/SkillType.hs new file mode 100644 index 0000000..3e533b8 --- /dev/null +++ b/src/Model/SkillType.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.SkillType where + +import GHC.Generics +import Data.Yaml + +import Model.Translated + +data SkillType = SkillType + { name :: Translated + , skills :: [Translated] + } deriving (Show, Read, Eq, Generic) + +instance FromJSON SkillType diff --git a/src/Model/Translated.hs b/src/Model/Translated.hs new file mode 100644 index 0000000..b92ae39 --- /dev/null +++ b/src/Model/Translated.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Model.Translated where + +import GHC.Generics +import Data.Yaml + +import Model.Translation.Language + +data Translated = Translated + { english :: String + , french :: String + } deriving (Show, Read, Eq, Generic) + +instance FromJSON Translated + +getTranslation :: Language -> Translated -> String +getTranslation English translated = english translated +getTranslation French translated = french translated diff --git a/src/Model/Translation/Key.hs b/src/Model/Translation/Key.hs new file mode 100644 index 0000000..43381fb --- /dev/null +++ b/src/Model/Translation/Key.hs @@ -0,0 +1,28 @@ +module Model.Translation.Key + ( Key(..) + ) where + +data Key = + + Home + | Resume + | Projects + | Contact + + | Experience + | EducationalBackground + | Skills + | Interests + + | Since + | From + | To + | And + + | Month Int + | MonthText Int + | YearText Int + + | TryIt + + | PageNotFound diff --git a/src/Model/Translation/Language.hs b/src/Model/Translation/Language.hs new file mode 100644 index 0000000..a8f5eb9 --- /dev/null +++ b/src/Model/Translation/Language.hs @@ -0,0 +1,12 @@ +module Model.Translation.Language + ( Language(..) + , languages + ) where + +data Language = + English + | French + deriving (Show, Read, Eq, Enum, Bounded) + +languages :: [Language] +languages = [minBound..] diff --git a/src/Model/Translation/Message.hs b/src/Model/Translation/Message.hs new file mode 100644 index 0000000..c9e0fcb --- /dev/null +++ b/src/Model/Translation/Message.hs @@ -0,0 +1,90 @@ +module Model.Translation.Message + ( getMessage + ) where + +import Model.Translation.Language +import Model.Translation.Key + +getMessage :: Key -> Language -> String + +getMessage Home English = "home" +getMessage Home French = "accueil" + +getMessage Resume English = "resume" +getMessage Resume French = "curriculum" + +getMessage Projects English = "projects" +getMessage Projects French = "projets" + +getMessage Contact English = "contact" +getMessage Contact French = "contact" + +getMessage Experience English = "experience" +getMessage Experience French = "expérience" + +getMessage EducationalBackground English = "educational background" +getMessage EducationalBackground French = "études" + +getMessage Skills English = "skills" +getMessage Skills French = "compétences" + +getMessage Interests English = "interests" +getMessage Interests French = "intérêts" + +getMessage Since English = "since" +getMessage Since French = "depuis" + +getMessage From English = "from" +getMessage From French = "de" + +getMessage To English = "to" +getMessage To French = "à" + +getMessage And English = "and" +getMessage And French = "et" + +getMessage (Month m) English = + case m of + 1 -> "january" + 2 -> "february" + 3 -> "march" + 4 -> "april" + 5 -> "may" + 6 -> "june" + 7 -> "july" + 8 -> "august" + 9 -> "september" + 10 -> "october" + 11 -> "november" + 12 -> "december" + _ -> "" +getMessage (Month m) French = + case m of + 1 -> "janvier" + 2 -> "février" + 3 -> "mars" + 4 -> "avril" + 5 -> "mai" + 6 -> "juin" + 7 -> "juillet" + 8 -> "août" + 9 -> "septembre" + 10 -> "octoble" + 11 -> "novembre" + 12 -> "décembre" + _ -> "" + +getMessage (MonthText count) English = "month" ++ (plural count) +getMessage (MonthText _) French = "mois" + +getMessage (YearText count) English = "year" ++ (plural count) +getMessage (YearText count) French = "an" ++ (plural count) + +getMessage TryIt English = "Try it!" +getMessage TryIt French = "Voir" + +getMessage PageNotFound English = "Page not found." +getMessage PageNotFound French = "La page que vous recherchez n'est pas disponible." + +plural :: Int -> String +plural count = if count > 1 then "s" else "" diff --git a/src/PDF.hs b/src/PDF.hs new file mode 100644 index 0000000..a002b38 --- /dev/null +++ b/src/PDF.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE OverloadedStrings #-} + +module PDF + ( generatePDFAt + ) where + +import Control.Exception (SomeException, try) +import System.Directory (copyFile, createDirectoryIfMissing) +import System.FilePath (takeDirectory) +import System.IO (hClose) +import System.IO.Temp (withTempFile) +import qualified System.Process as Process (callCommand) +import Text.LaTeX (LaTeX, renderFile) + +generatePDFAt :: FilePath -> LaTeX -> IO () +generatePDFAt path latex = do + (basePath, tmpPath) <- generatePDF latex + createDirectoryIfMissing True (takeDirectory path) + copyFile tmpPath path + Process.callCommand ("rm " ++ basePath ++ "*") + +generatePDF :: LaTeX -> IO (FilePath, FilePath) +generatePDF latex = + withTempFile "/tmp" "latex" $ \filePath handle -> do + hClose handle + renderFile filePath latex + renderFile "/home/joris/resume.tex" latex + pdfCommand filePath + pdfCommand filePath + return (filePath, filePath ++ ".pdf") + +pdfCommand :: String -> IO () +pdfCommand path = do + let command = "cd resume && pdflatex --output-directory /tmp " ++ path ++ " >/dev/null" + result <- try $ Process.callCommand command :: IO (Either SomeException ()) + case result of + Left err -> do + putStrLn "Error generating PDF:" + putStrLn (show err) + Right _ -> + return () diff --git a/src/Resume.hs b/src/Resume.hs new file mode 100644 index 0000000..9b691c6 --- /dev/null +++ b/src/Resume.hs @@ -0,0 +1,34 @@ +module Resume + ( generateResumes + , resumePath + ) where + +import Conf (Conf) + +import Model +import Model.Date (Date) +import Model.Identity (Identity(name)) +import Model.Translation.Language +import Model.Translation.Message (getMessage) +import qualified Model.Translation.Key as K + +import View.LaTeX.Resume (resumeLaTeX) + +import Date (getCurrentDate) + +import PDF (generatePDFAt) + +generateResumes :: Model -> Conf -> IO () +generateResumes model conf = do + currentDate <- getCurrentDate + mapM_ (generateResume model currentDate conf) languages + +generateResume :: Model -> Date -> Conf -> Language -> IO () +generateResume model date conf language = + let path = "public/" ++ (resumePath model language) + resume = resumeLaTeX conf language date model + in generatePDFAt path resume + +resumePath :: Model -> Language -> FilePath +resumePath model language = + "resumes/" ++ (getMessage K.Resume language) ++ " " ++ (name . identity $ model) ++ ".pdf" diff --git a/src/Utils/String.hs b/src/Utils/String.hs new file mode 100644 index 0000000..44cdf03 --- /dev/null +++ b/src/Utils/String.hs @@ -0,0 +1,21 @@ +module Utils.String + ( capitalizeWords + , capitalizeFirstWord + , capitalizeWord + ) where + +import Data.Char + +capitalizeWords :: String -> String +capitalizeWords = unwords . map capitalizeWord . words + +capitalizeFirstWord :: String -> String +capitalizeFirstWord = unwords . mapFirst capitalizeWord . words + +capitalizeWord :: String -> String +capitalizeWord [] = [] +capitalizeWord (x:xs) = toUpper x : map toLower xs + +mapFirst :: (a -> a) -> [a] -> [a] +mapFirst _ [] = [] +mapFirst f (x:xs) = f x : xs diff --git a/src/View/Git.hs b/src/View/Git.hs new file mode 100644 index 0000000..abf5226 --- /dev/null +++ b/src/View/Git.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.Git + ( renderGit + , renderGitIcon + ) where + +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5.Attributes +import qualified Text.Blaze.Html5 as H + +import Conf (Conf) +import qualified Conf as Conf + +import View.Icon (renderIcon) + +renderGit :: Conf -> String -> Html +renderGit conf ref = + H.div + ! class_ "git" + $ H.a + ! href (fromString $ gitLink conf ref) + $ do + renderIcon "git-square" + fromString ref + +renderGitIcon :: Conf -> String -> Html +renderGitIcon conf ref = + H.a + ! class_ "git" + ! href (fromString $ gitLink conf ref) + $ renderIcon "git-square" + +gitLink :: Conf -> String -> String +gitLink conf ref = (Conf.git conf) ++ ref diff --git a/src/View/Header.hs b/src/View/Header.hs new file mode 100644 index 0000000..a29b9a3 --- /dev/null +++ b/src/View/Header.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.Header + ( renderHeader + ) where + +import Control.Monad (forM_) + +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5 +import Text.Blaze.Html5.Attributes +import qualified Text.Blaze.Html5 as H + +import Model.Header +import Model.Translation.Language +import Model.Translation.Message + +renderHeader :: Language -> Maybe Header -> Html +renderHeader language mbCurrentHeader = + H.div ! class_ "header" $ + ul $ + forM_ allHeaders (headerItem language mbCurrentHeader) + +headerItem :: Language -> Maybe Header -> Header -> Html +headerItem language mbCurrentHeader pageHeader = + li $ a + ! class_ (fromString $ (headerClass pageHeader) ++ " " ++ (if mbCurrentHeader == Just pageHeader then "currentHeader" else "")) + ! href (fromString . headerLink $ pageHeader) $ do + i ! class_ (fromString $ "fa fa-lg " ++ (headerIcon pageHeader)) $ "" + H.span + ! class_ "text" + $ fromString (getMessage (headerKey pageHeader) language) + +headerClass :: Header -> String +headerClass Resume = "resume" +headerClass Projects = "projects" + +headerIcon :: Header -> String +headerIcon Resume = "fa-user" +headerIcon Projects = "fa-flask" diff --git a/src/View/Icon.hs b/src/View/Icon.hs new file mode 100644 index 0000000..67db0a9 --- /dev/null +++ b/src/View/Icon.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.Icon + ( renderIcon + ) where + +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5 +import Text.Blaze.Html5.Attributes + +renderIcon :: String -> Html +renderIcon iconName = + i + ! class_ (fromString $ "fa fa-fw fa-" ++ iconName) + $ "" diff --git a/src/View/Interval.hs b/src/View/Interval.hs new file mode 100644 index 0000000..05cb62c --- /dev/null +++ b/src/View/Interval.hs @@ -0,0 +1,39 @@ +module View.Interval + ( renderDurationAndInterval + , renderYearInterval + , renderDuration + ) where + +import Data.Maybe (fromMaybe) + +import Model.Date +import Model.Translation.Language +import qualified Model.Translation.Key as K +import Model.Translation.Message + +renderDurationAndInterval :: Language -> Date -> Date -> Maybe Date -> String +renderDurationAndInterval language currentDate beginDate mbEndDate = + let duration = renderDuration language beginDate (fromMaybe currentDate mbEndDate) + interval = renderYearInterval language beginDate mbEndDate + in duration ++ ", " ++ interval + +renderDuration :: Language -> Date -> Date -> String +renderDuration language d1 d2 = + let (years, months) = yearAndMonthDiff d1 d2 + renderYears = (show years) ++ " " ++ (getMessage (K.YearText years) language) + renderMonths = (show months) ++ " " ++ (getMessage (K.MonthText months) language) + spaceAnd = " " ++ (getMessage K.And language) ++ " " + in if years > 0 then + renderYears ++ (if months > 0 then spaceAnd ++ renderMonths else "") + else + renderMonths + +renderYearInterval :: Language -> Date -> (Maybe Date) -> String +renderYearInterval language beginDate Nothing = + (getMessage K.Since language) ++ " " ++ (show . year $ beginDate) +renderYearInterval language beginDate (Just endDate) = + let beginYear = year beginDate + endYear = year endDate + in if beginYear == endYear + then show beginYear + else (show beginYear) ++ " " ++ (getMessage K.To language) ++ " " ++ (show endYear) diff --git a/src/View/LaTeX/Resume.hs b/src/View/LaTeX/Resume.hs new file mode 100644 index 0000000..6ec717f --- /dev/null +++ b/src/View/LaTeX/Resume.hs @@ -0,0 +1,148 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.LaTeX.Resume + ( resumeLaTeX + ) where + +import Text.LaTeX +import Text.LaTeX.Base.Syntax (LaTeX (TeXComm), TeXArg (FixArg)) + +import Data.Maybe (fromMaybe, listToMaybe) + +import Conf (Conf) +import qualified Conf as Conf + +import Model (Model) +import qualified Model as M +import qualified Model.Company as C +import Model.Date (Date) +import Model.Degree (Degree) +import qualified Model.Degree as D +import Model.Identity (Identity) +import qualified Model.Identity as I +import Model.Job (Job) +import qualified Model.Job as J +import qualified Model.School as S +import Model.SkillType (SkillType) +import qualified Model.SkillType as ST + +import Model.Translated +import Model.Translation.Key (Key) +import qualified Model.Translation.Key as K +import Model.Translation.Language +import Model.Translation.Message + +import View.Interval (renderDurationAndInterval) + +import Utils.String + +resumeLaTeX :: Conf -> Language -> Date -> Model -> LaTeX +resumeLaTeX conf language currentDate model = + preamble + <> document (body conf language currentDate model) + +preamble :: LaTeX +preamble = documentclass [] "resume" + +body :: Conf -> Language -> Date -> Model -> LaTeX +body conf language currentDate model = + identityLaTeX conf language (M.identity model) (M.jobs model) + <> jobsLaTeX language currentDate (M.jobs model) + <> degreesLaTeX language (M.degrees model) + <> skillTypesLaTeX language (M.skillTypes model) + <> interestsLaTeX language (M.interests model) + +identityLaTeX :: Conf -> Language -> Identity -> [Job] -> LaTeX +identityLaTeX conf language identity jobs = + TeXComm "header" + [ FixArg (fromString $ I.name identity) + , FixArg (fromString . fromMaybe "" . (fmap (getTranslation language . J.name)) . listToMaybe $ jobs) + , FixArg (fromString $ I.website identity) + , FixArg (fromString $ Conf.git conf) + , FixArg (fromString $ I.git identity) + ] + +jobsLaTeX :: Language -> Date -> [Job] -> LaTeX +jobsLaTeX language currentDate jobs = + translatedSection K.Experience language + <> mconcat (map (jobLaTeX language currentDate) jobs) + +jobLaTeX :: Language -> Date -> Job -> LaTeX +jobLaTeX language currentDate job = + customCommand "position" + [ fromString . getTranslation language $ J.name job + , fromString $ renderDurationAndInterval language currentDate (J.beginDate job) (J.endDate job) + , fromString $ ((C.name . J.company) job) ++ ", " ++ ((C.location . J.company) job) + , (fromString . getTranslation language . J.description $ job) + , case J.details job of + Just details@(_:_) -> translatedBullets language details + _ -> mempty + , case J.technos job of + Just ts -> technos ts + _ -> mempty + ] + +degreesLaTeX :: Language -> [Degree] -> LaTeX +degreesLaTeX language degrees = + translatedSection K.EducationalBackground language + <> mconcat (map (degreeLaTeX language) degrees) + +degreeLaTeX :: Language -> Degree -> LaTeX +degreeLaTeX language degree = + customCommand "position" + [ fromString . getTranslation language $ D.name degree + , fromString . show . D.year $ degree + , fromString $ S.name school ++ (fromMaybe "" (((++) ", ") <$> S.location school)) + , mempty + , translatedBullets language $ D.topics degree + , mempty + ] + where school = D.school degree + +skillTypesLaTeX :: Language -> [SkillType] -> LaTeX +skillTypesLaTeX language skillTypes = + translatedSection K.Skills language + <> (mconcat . map (skillTypeLaTeX language) $ skillTypes) + +skillTypeLaTeX :: Language -> SkillType -> LaTeX +skillTypeLaTeX language skillType = + subsection (fromString . capitalizeFirstWord . getTranslation language . ST.name $ skillType) + <> translatedBullets language (ST.skills skillType) + +interestsLaTeX :: Language -> [Translated] -> LaTeX +interestsLaTeX language interests = + translatedSection K.Interests language + <> translatedBullets language interests + +translatedBullets :: Language -> [Translated] -> LaTeX +translatedBullets language = itemsEnv "bullets" . map (getTranslation language) + +itemsEnv :: String -> [String] -> LaTeX +itemsEnv name items = + environment name . mconcat . concat . map getItem $ items + where getItem i = + [ item Nothing + , fromString . capitalizeFirstWord $ i + ] + +technos :: [String] -> LaTeX +technos names = + environment "technos" . mconcat . concat . map getItem $ names + where getItem name = + [ item Nothing + , TeXComm "techno" [ FixArg (fromString name) ] + ] + +translatedSection :: Key -> Language -> LaTeX +translatedSection key language = + section (fromString . capitalizeWords $ getMessage key language) + +customCommand :: String -> [LaTeX] -> LaTeX +customCommand commandName commandParameters = + TeXComm commandName (map FixArg commandParameters) + +environment :: String -> LaTeX -> LaTeX +environment name inside = + (TeXComm "begin" [ FixArg (fromString name) ]) + <> inside + <> (TeXComm "end" [ FixArg (fromString name) ]) diff --git a/src/View/NotFound.hs b/src/View/NotFound.hs new file mode 100644 index 0000000..c16eb6c --- /dev/null +++ b/src/View/NotFound.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.NotFound + ( renderNotFound + ) where + +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5 +import Text.Blaze.Html5.Attributes +import qualified Text.Blaze.Html5 as H + +import Model.Translation.Language +import qualified Model.Translation.Key as K +import Model.Translation.Message + +import View.Header (renderHeader) + +renderNotFound :: Language -> Html +renderNotFound language = + H.div $ do + renderHeader language Nothing + H.div ! class_ "notFoundPage" $ do + h1 "404" + p .fromString $ getMessage K.PageNotFound language diff --git a/src/View/Page.hs b/src/View/Page.hs new file mode 100644 index 0000000..b7267e8 --- /dev/null +++ b/src/View/Page.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.Page + ( renderPage + ) where + +import Data.Text.Internal.Lazy +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5 +import Text.Blaze.Html5.Attributes +import qualified Text.Blaze.Html5 as H +import Text.Blaze.Html.Renderer.Text (renderHtml) + +import qualified Model as M +import qualified Model.Identity as I + +renderPage :: M.Model -> Html -> Text +renderPage model page = + renderHtml $ do + docTypeHtml $ do + H.head $ do + H.title $ fromString . I.name . M.identity $ model + meta ! charset "UTF-8" + meta ! name "viewport" ! content "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" + meta ! name "author" ! content (toValue $ I.name . M.identity $ model) + meta ! name "description" ! content (toValue $ M.description model) + link ! rel "stylesheet" ! type_ "text/css" ! href "/stylesheets/reset.css" + link ! rel "stylesheet" ! href "/stylesheets/font-awesome-4.2.0/css/font-awesome.min.css" + link ! rel "stylesheet" ! type_ "text/css" ! href "/design" + link ! rel "icon" ! type_ "image/png" ! href "/images/icon.png" + H.body page diff --git a/src/View/Project.hs b/src/View/Project.hs new file mode 100644 index 0000000..a5aaf2c --- /dev/null +++ b/src/View/Project.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.Project + ( renderProjects + ) where + +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5 +import Text.Blaze.Html5.Attributes +import qualified Text.Blaze.Html5 as H + +import Conf (Conf) + +import Model.Header (Header(Projects)) +import qualified Model.Project as P +import Model.Translated +import Model.Translation.Language + +import View.Header (renderHeader) +import View.Git (renderGitIcon) + +renderProjects :: Conf -> Language -> [P.Project] -> Html +renderProjects conf language projects = + H.div $ do + renderHeader language (Just Projects) + H.div ! class_ "projectsPage" $ + mapM_ (renderProject conf language) projects + +renderProject :: Conf -> Language -> P.Project -> Html +renderProject conf language project = + H.div ! class_ "project" $ do + renderTitle conf project + H.div ! class_ "body" $ do + renderTechnologies (P.technologies project) + case P.pageLink project of + Just pageLink -> renderPageLink pageLink + Nothing -> fromString "" + case P.description project of + Just description -> renderDescription language description + Nothing -> H.div "" + +renderTitle :: Conf -> P.Project -> Html +renderTitle conf project = + h1 $ do + toHtml (P.name project) + H.span ! class_ "separator" $ fromString " − " + renderGitIcon conf (P.git project) + +renderTechnologies :: [String] -> Html +renderTechnologies technologies = + H.div ! class_ "technologies" $ do + ul $ mapM_ (H.li . fromString) technologies + +renderPageLink :: String -> Html +renderPageLink pageLink = + H.div ! class_ "pageLink" $ do + H.a + ! href (fromString pageLink) + $ toHtml pageLink + +renderDescription :: Language -> Translated -> Html +renderDescription language description = + H.div ! class_ "description" $ do + fromString . getTranslation language $ description diff --git a/src/View/Resume.hs b/src/View/Resume.hs new file mode 100644 index 0000000..ffd5c8c --- /dev/null +++ b/src/View/Resume.hs @@ -0,0 +1,140 @@ +{-# LANGUAGE OverloadedStrings #-} + +module View.Resume + ( renderResume + ) where + +import Data.Maybe +import Data.String (fromString) + +import Text.Blaze.Html +import Text.Blaze.Html5 hiding (details) +import qualified Text.Blaze.Html5 as H +import Text.Blaze.Html5.Attributes + +import Conf (Conf) + +import qualified Model as M +import qualified Model.Company as C +import Model.Date +import qualified Model.Degree as D +import Model.Header (Header (Resume)) +import qualified Model.Identity as I +import qualified Model.Job as J +import qualified Model.School as S +import qualified Model.SkillType as ST +import Model.Translated +import Model.Translation.Key (Key) +import qualified Model.Translation.Key as K +import Model.Translation.Language +import Model.Translation.Message + +import View.Git (renderGit) +import View.Header (renderHeader) +import View.Icon (renderIcon) +import View.Interval (renderDurationAndInterval) + +import Resume (resumePath) + +renderResume :: Conf -> Language -> Date -> M.Model -> Html +renderResume conf language currentDate model = + H.div $ do + renderHeader language (Just Resume) + H.div ! class_ "resumePage" $ do + renderIdentity conf language model + renderJobs language currentDate . M.jobs $ model + renderDegrees language . M.degrees $ model + renderSkillTypes language . M.skillTypes $ model + renderInterests language . M.interests $ model + +renderIdentity :: Conf -> Language -> M.Model -> Html +renderIdentity conf language model = + let identity = M.identity model + in H.div ! class_ "section" $ do + H.div ! class_ "identity" $ do + h1 (fromString (I.name identity)) + renderGit conf (I.git identity) + a ! href (fromString $ resumePath model language) ! class_ "pdf" ! target "_blank" $ + renderIcon "print" + +renderJobs :: Language -> Date -> [J.Job] -> Html +renderJobs language currentDate jobs = + H.div ! class_ "section" $ do + sectionTitle K.Experience language + mapM_ (renderJob language currentDate) jobs + +renderJob :: Language -> Date -> J.Job -> Html +renderJob language currentDate job = + H.div ! class_ "item" $ do + + H.div ! class_ "title" $ do + H.div ! class_ "text" $ + fromString . getTranslation language . J.name $ job + H.div ! class_ "date" $ + fromString $ renderDurationAndInterval language currentDate (J.beginDate job) (J.endDate job) + + H.div ! class_ "location" $ do + let company = J.company job + companyName = C.name company + companyLocation = C.location company + fromString $ companyName ++ ", " ++ companyLocation + + H.div ! class_ "description" $ do + _ <- fromString . getTranslation language . J.description $ job + case J.details job of + Just details -> + ul ! class_ "bullets detail" $ + mapM_ (\detail -> li . fromString . getTranslation language $ detail) details + Nothing -> + fromString "" + renderTechnos $ fromMaybe [] (J.technos job) + +renderTechnos :: [String] -> Html +renderTechnos = (ul ! class_ "technos") . mapM_ ((li ! class_ "techno") . fromString) + +renderDegrees :: Language -> [D.Degree] -> Html +renderDegrees language degrees = + H.div ! class_ "section" $ do + sectionTitle K.EducationalBackground language + mapM_ (renderDegree language) degrees + +renderDegree :: Language -> D.Degree -> Html +renderDegree language degree = + H.div ! class_ "item" $ do + + H.div ! class_ "title" $ do + H.div ! class_ "text " $ + fromString . getTranslation language . D.name $ degree + H.div ! class_ "date" $ + fromString . show . D.year $ degree + + H.div ! class_ "location" $ + let school = D.school degree + location = fromMaybe "" $ fmap (", " ++ ) (S.location school) + in fromString $ (S.name school) ++ location + + ul ! class_ "bullets itemList" $ + mapM_ (\topic -> li . fromString . getTranslation language $ topic) (D.topics degree) + +renderSkillTypes :: Language -> [ST.SkillType] -> Html +renderSkillTypes language skillTypes = + H.div ! class_ "section" $ do + sectionTitle K.Skills language + mapM_ (renderSkillType language) skillTypes + +renderSkillType :: Language -> ST.SkillType -> Html +renderSkillType language skillType = + H.div ! class_ "item" $ do + H.div ! class_ "title skills" $ H.div ! class_ "text" $ + fromString . getTranslation language . ST.name $ skillType + ul ! class_ "bullets itemList" $ + mapM_ (\skill -> li . fromString . getTranslation language $ skill) (ST.skills skillType) + +renderInterests :: Language -> [Translated] -> Html +renderInterests language interests = + H.div ! class_ "section" $ do + sectionTitle K.Interests language + ul ! class_ "bullets" $ mapM_ (\interest -> li . fromString . getTranslation language $ interest) interests + +sectionTitle :: Key -> Language -> Html +sectionTitle key language = h1 . fromString $ getMessage key language -- cgit v1.2.3