ТОП-13 крутых идей веб-проектов для прокачки навыков

0
30317

Иногда хочется написать что-то эдакое, а идей нет. Вот вам подборка классных идей для веб-проектов на любой случай и вкус.


Данные приложения поспособствуют:

  • поднятию уровня знаний по программированию;
  • изучению новых технологий;
  • созданию нового проекта в портфолио;
  • созданию продукта, который можно легко расширить доп. возможностями.

ТОП-13 крутых идей веб-проектов для прокачки навыков

Список идей разделен на три базовых уровня сложности:

  • Beginner – разработчики  на ранних этапах своего учебного путешествия, которые обычно сосредоточены на создании простых веб-проектов.
  • Intermediate – разработчики с определенным количеством опыта. Они себя хорошо чувствуют в создании UI/UX, в использовании инструментов разработки и написании приложений, использующих API.
  • Advanced – программисты, имеющие все вышеперечисленное и создающие более продвинутые штуки, такие как бэкенд и службы БД.

1. Блокнот

Уровень: beginner

Описание: создание и сохранение заметок.

Возможности:

  • сохранение;
  • редактирование;
  • удаление;
  • при закрытии окна / браузера данные должны сохраняться, а при "возвращении" пользователя – динамически подгружаться.

Бонус:

  • пользователь может редактировать заметку в Markdown-формате; при сохранении результат конвертируется в HTML;
  • пользователю видна дата последнего изменения заметки.

Полезные ссылки:

Пример:

HTML

<h1 class="header">AngularJS Markdown Notes</h1>
<div ng-app="notesApp" class="app">
  <div ng-controller="notesController">
  
    <span class="icons">
    <i class="fa fa-plus-circle fa-2x" ng-click="addNote()"></i>
      </span>
    <div class="note" ng-repeat="note in notes | orderBy:'createdOn':true track by $index">
      <span class="date">{{note.createdOn | date:"EEEE dd MMMM, yyyy 'at' h:mma"}}</span>
      <span class="icons">
      <i class="fa fa-check fa-lg" ng-if="note.edit == false"></i>
      <i class="fa fa-pencil fa-lg" ng-if="note.edit == true"></i>
      <i class="fa fa-trash-o fa-lg" ng-click="delete(notes.length - $index - 1)"></i></span>
      
      <div class="markdown-body" ng-model="note.text" ng-change="update(notes.length - 
        $index - 1,note.text)" ng-focus="note.edit = true" ng-blur="note.edit = 
        false" markdown-editable contenteditable="true">{{ note.text }}</div>
    </div>
  </div>
</div>

CSS

@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700);
@import url(https://fonts.googleapis.com/css?family=Source+Code+Pro);

* {
  box-sizing:border-box;
}

body {
  max-width:580px;
  margin:auto;
  padding:10px;
  position:relative;
  background: #fdfdfd;
}

img {
  max-width:100%;
  height:auto;
}

div[contenteditable] {
  font-size:1.2em;
  font-family: 'Source Sans Pro', sans-serif;
  border:none;
  box-shadow:none;
  margin:0;
  width:100%;
  padding:10px 20px 10px;
  min-height:300px;
  background:#f9f9f9;
  
  pre, code {
    background:#ededed;
  }
}

div[contenteditable]:focus{
 font-family: 'Source Code Pro', courier, monospace;
  font-size:1em;
}

.header {
  margin:5px 10px 0;
  color:#d46e58;
  display:inline-block;
}

.note {
  background: #eee;
  box-shadow:#999 1px 1px 3px;
  margin:30px 0;
  padding-top:40px;
  min-height:200px;
  width:100%;
  display:block;
  position:relative;
  overflow:hidden;
}

.date {
  position:absolute;
  top:0;
  left:0;
  padding:15px;
  font-size:0.7em;
  font-style: italic;
  color:#71CBD0;
}

.icons {
  position:absolute;
  right:0;
  top:0;
  padding:10px;
}

.fa-trash-o, .fa-plus-circle {
  cursor:pointer;
}

.fa-check {
  color:#92D788;
}

.fa-trash-o {
  color:#C2474B;
  
  &:hover {
    color:darken(#C2474B,20);
  }
}

.fa-pencil {
  color:#DBC394;
}

.fa-plus-circle {
  color:#71CBD0;
  
  &:hover {
    color:darken(#71CBD0,20);
  }
}

JS

(function(){
  var app = angular.module('notesApp',['angular-markdown-editable']); 
  app.controller('notesController', function($scope){
    
    // Initial Data
  	$scope.notes = [{
      createdOn:1428237500771,
      edit:false,
      text:"#Hello, World!\n\n
           Delete this note\n\nMarkdown compiled using the fantastic 
           [angular-markdown-editable](http://projects.quiver.
           is/angular-markdown-editable/) directive."
    }];
    
    // Add New Note
    $scope.addNote = function(){
      $scope.newNote = {};
      $scope.newNote.createdOn = Date.now();
      $scope.newNote.text = ' ';
      $scope.newNote.edit = true;
      $scope.notes.push($scope.newNote);
      $scope.newNote = {};
    };
    // Delete Note
    $scope.delete = function (i) {
      var r = confirm("Are you sure you want to delete this note?");
      if (r == true) 
        $scope.notes.splice(i, 1);
		};
    // Update Note
    $scope.update = function(i, note) {
      $scope.notes[i].text = note;
      $scope.notes[i].edit = false;
    };
  // End Controller
  });
// End Function  
})();

2. Рождественские огни

Уровень: beginner

Описание: ваша задача нарисовать в ряд семь цветных окружностей и по таймеру изменять интенсивность свечения каждого из них. После прохода нескольких рядов "лампочек" последовательность подсвечивания меняется.

Возможности:

  • пользователь может нажать кнопку для включения/выключения огней;
  • пользователь может изменить время между интервалами свечения.

Бонус:

  • выбор цвета заполнения каждой окружности;
  • указание значения интенсивности;
  • изменение размера любого круга в ряду;
  • указание количества строк (от 1 до 7).

Пример:

HTML

<ul class="lightrope">
  <li></li>
  <li></li>
  <li></li>
  ***
  ***
  <li></li>
  <li></li>
</ul>

CSS

$globe-width:   12px;
$globe-height:  28px;
$globe-spacing: 40px;
$globe-spread: 3px;
$light-off-opacity: 0.4;

body {
  background: #000;
}
.lightrope {
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  position: absolute;
  z-index: 1;
  margin: -15px 0 0 0;
  padding: 0;
  pointer-events: none;
  width: 100%;
  li {
    position: relative;
    animation-fill-mode: both; 
    animation-iteration-count:infinite;
    list-style: none;
    margin: 0;
    padding: 0;
    display: block;
    width: $globe-width;
    height: $globe-height;
    border-radius: 50%;
    margin: $globe-spacing/2;
    display: inline-block;
    background: rgba(0,247,165,1);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(0,247,165,1);
    animation-name: flash-1;
    animation-duration: 2s;
    &:nth-child(2n+1) {
      background: rgba(0,255,255,1);
      box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(0,255,255,0.5);
      animation-name: flash-2;
      animation-duration: 0.4s;
    }
    &:nth-child(4n+2) {
      background: rgba(247,0,148,1);
      box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(247,0,148,1);
      animation-name: flash-3;
      animation-duration: 1.1s;
    }
    &:nth-child(odd) {
      animation-duration: 1.8s;
    }
    &:nth-child(3n+1) {
      animation-duration: 1.4s;
    }
    &:before {
      content: "";
      position: absolute;
      background: #222;
      width: ($globe-width - 2);
      height: $globe-height/3;
      border-radius: 3px;
      top: (0 - ($globe-height/6));
      left: 1px;
    }
    &:after {
      content: "";
      top: (0 - $globe-height/2);
      left: $globe-width - 3;
      position: absolute;
      width: $globe-spacing + 12;
      height: ($globe-height/3 * 2);
      border-bottom: solid #222 2px;
      border-radius: 50%;
    }
    &:last-child:after {
      content: none;
    }
    &:first-child {
      margin-left: -$globe-spacing;
    }
  }
}
@keyframes flash-1 { 
    0%, 100% { background: rgba(0,247,165,1);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(0,247,165,1);} 
    50% { background: rgba(0,247,165,$light-off-opacity);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(0,247,165,0.2);}
}
@keyframes flash-2 { 
    0%, 100% { background: rgba(0,255,255,1);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(0,255,255,1);} 
    50% { background: rgba(0,255,255,$light-off-opacity);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(0,255,255,0.2);}
}
@keyframes flash-3 { 
    0%, 100% { background: rgba(247,0,148,1);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(247,0,148,1);} 
    50% { background: rgba(247,0,148,$light-off-opacity);
    box-shadow: 0px $globe-height/6 $globe-width*2 $globe-spread rgba(247,0,148,0.2);}
}

Следующий вариант из списка веб-проектов часто приходится реализовывать на практике.

3. Тестирование

Уровень: beginner

Описание: создайте приложение-опрос для тестирования навыков программирования разработчиков.

Возможности:

  • запуск теста по кнопке;
  • отображение 4 вариантов ответа на вопрос;
  • после выбора ответа необходимо показывать следующий вопрос до конца теста;
  • в конце пользователю оглашается результат тестирования (общее время прохождения теста, количество правильных ответов, сообщение статуса прохождения теста).

Бонус:

  • пользователь может поделиться результатами в социальных сетях;
  • выбор нескольких направлений для проверки;
  • личный кабинет юзера, в котором хранится информация по всем тестам;
  • тест можно проходить несколько раз.

Полезные ссылки:

Пример:

HTML

.quiz-container
  h1 Quiz interface
  .question.box
    p <span>1.</span> What does CSS stand for?
    
  ul.answers
    li.answer.box
      p <span>a</span> Computer Style Sheets
    li.answer.box
      p <span>b</span> Cascading Style Sheets
    li.answer.box
      p <span>c</span> Colorful Style Sheets
    li.answer.box
      p <span>d</span> Creative Style Sheets
      
  button.box Send
    span ›

CSS

@import url('https://fonts.googleapis.com/css?family=Lato');

$red : #cb2127;
$green : #2ecc71;

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body, html {
  height: 100%;
}

body {
  font-family: 'Lato', sans-serif;
  display: flex;
  flex-direction: column;
  justify-content: center;
  
  &:before {
    content: "";
    display: block;
    position:fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background-image: url("http://oi66.tinypic.com/23mvj48.jpg");
    background-size: cover;
    background-repeat: no-repeat;
    
    filter: blur(2px);
    z-index: -1;
  }
  
}

.quiz-container{
  width: 450px;
  color: white;
  font-size: 110%;
  margin: auto;
  
  h1 {
    text-align: center;
    color: $red;
    margin-bottom: 10px;
  }
  
  .box {
    padding: 10px 15px;
  }
  
  .question {
    background-color: $red;
    margin-bottom: 25px;
    font-size: 115%;
    padding-top: 20px;
    padding-bottom: 20px;
    position: relative;
    
    &:after {
      content: "";
      position: absolute;
      bottom: -15px;
      left: 35px;
      width: 0; 
      height: 0; 
      border-left: 15px solid transparent;
      border-right: 15px solid transparent;

      border-top: 15px solid $red;
    }
    
    span {
      background-color: $red;
      color: white;
      display: block;
      float: left;
      margin-left: -15px;
      margin-top: -10px;
      margin-right: 0px;
      padding: 10px 10px;
      text-align: center;
      text-transform: uppercase;
      width: 40px;
    }
  }
  
  .answers {
    padding-left: 0;
    list-style-type: none;
    
    .answer {
      background-color: #fff;
      color: $red;
      margin-bottom: 10px;
      position: relative;
      
      &:hover, &.active{
        cursor: pointer;
        color: $green;
        
        span:not([class^="checkmark"]) {
          background-color: $green;
        }
      }
      
      &.active span.checkmark {
        background-color: #fff;
        position: absolute;
        top: 8px;
        right: 20px;
        font-size: 120%;
      }
      
        span:not([class^="checkmark"]) {
          background-color: $red;
          color: white;
          display: block;
          float: left;
          margin-left: -15px;
          margin-top: - 10px;
          margin-right: 20px;
          padding: 10px 0;
          text-align: center;
          text-transform: uppercase;
          width: 40px;
       }
    }
    
  }
  
  button {
    background-color: $red;
    color: #fff;
    font-size: 110%;
    border: 0;
    width: 100%;
    position: relative;
    
    &:hover {
      background-color: $green;
      cursor:pointer;
    }
    
    span {
      font-size: 200%;
      position: absolute;
      top: -3px;
      right: 10px;
    }
  }
}

@media (max-width: 450px) {
  .quiz-container {
    width: calc(100% - 30px);
  }
}

JS

$(document).ready(function(){
  var checkmark = "<span class='checkmark'>✓</span>";
  
  $(".answer").click(function() {
    $(this).siblings(".answer").removeClass("active").children("span").remove();
    $(this).addClass("active").append(checkmark);
  });
  
  $("button").click(function() {
    if($(".active").length) {
      if($(".active").index() === 1) {
        alert("Well done!");
      } else {
        alert("Wrong answer!");
      }
    } else {
      alert("Please select an answer!");
    }
  });
});

4. Конвертер римских чисел в десятичные

Уровень: beginner

Описание: римские цифры включают в себя семь символов, каждый с фиксированным целым значением:

  • I – 1
  • V – 5
  • X – 10
  • L – 50
  • C – 100
  • D – 500
  • M – 1000

Возможности:

  • возможность ввода римских цифр в поле ввода;
  • отображение результата в другом поле после нажатия на кнопку;
  • если введен неправильный символ, юзер должен увидеть ошибку.

Бонус:

  • автоматическое преобразование во время ввода;
  • возможность конвертирования в "обе стороны".

Полезные ссылки:

Пример:

Конвертер

5. Поиск книг

Уровень: intermediate

Описание: приложение для поиска книги по запросу (название, автор и т. д.). Список книг отображается со всей информацией о них.

Возможности:

  • пользователь может ввести поисковой запрос в поле ввода;
  • юзер может отправить запрос, который обращается к API, возвращающей массив с книгами и дополнительной информацией (название, автор, дата публикации и т. д).

Бонус:

  • для каждого элемента в списке добавьте ссылку, перенаправляющую на сайт с дополнительной информацией о книге;
  • реализовать адаптивный дизайн;
  • добавить анимацию загрузки.

Полезные ссылки:

Пример:

HTML

<div id="app">
  <form id="book-search" @submit.prevent="search">
    <label>
      <span>Search for a book</span>
      
      <input type="text" v-model:trim="searchTerm" placeholder="⌕">
    </label>
    
    <button type="submit">Search</button>
  </form>
  
  <ol class="search-results">
    <li class="search-result" v-for="book in searchResults.items">
      <img :src="'http://books.google.com/books/content?id=' + book.id + 
      '&printsec=frontcover&img=1&zoom=1&source=gbs_api'" class="search-result--thumbnail">
      
      <ul class="search-result--info">
        <li class="search-result--title">{{ book.volumeInfo.title }}</li>
        
        <li v-if="book.volumeInfo.authors" class="search-result--authors">
          by {{ bookAuthors(book) }}
        </li>
        
        <li v-if="book.volumeInfo.publishedDate" class="search-result--published">
          <span>Published </span> {{ book.volumeInfo.publishedDate }}
        </li>
      </ul>
    </li>
  </ol>
  
  <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/106403/allthebooks%5B1%5D.jpg" 
       id="all-the-books" v-if="searchResults.items">
</div>

CSS

@function between($to, $from, $toWidth, $fromWidth) {
  $slope: ($to - $from) / ($toWidth - $fromWidth);
  $base: $from - $slope * $fromWidth;

  @return calc(#{$base} + #{100vw * $slope});
}

$small: 550px;
$large: 1600px;

:root {
  font-size: between(24px, 16px, $large, $small);
}

*,*:before,*:after { box-sizing: inherit }

html  {
  box-sizing: border-box;
  height: 100%;
  display: flex;
}

body {
  margin: auto;
  background-color: #faf7f6;
  color: #805466;
}

#app {
  padding: 5%;
}

#book-search {
  display: flex;
  align-items: flex-end;
}

label span {
  display: block;
  font-size: .9rem;
  font-weight: bold;
}

input {
  padding: .2rem .5rem;
  border-radius: .25rem;
  border: 2px solid #f3edeb;
  margin-top: .25rem;
  margin-right: .25rem;
  box-shadow: 0 .4rem 1rem -.4rem #d4b1a5;
}

button {
  background: rebeccapurple;
  position: relative;
  color: white;
  border: none;
  padding: 0 .75rem;
  border-radius: .25rem;
  font-size: .6rem;
  line-height: 2.8;
  text-transform: uppercase;
  box-shadow: 0 .4rem 1rem -.4rem purple;
}

.search-results {
  padding-left: 0;
  
  li + li {
    margin-top: .5rem
  }
}

.search-result {
  background-color: white;
  position: relative;
  border: 2px solid #f3edeb;
  border-radius: .25rem;
  overflow: hidden;
  box-shadow: 0 1rem 1rem -.75rem #dfc8c0;
  display: flex;
  align-items: center;
}

.search-result--thumbnail {
  flex: none;
  width: 15%;
  width: 4rem;
  height: auto;
  align-self: flex-start;
}

.search-result--info {
  display: flex;
  height: 100%;
  flex-direction: column;
  justify-content: space-around;
  padding: .5rem;
  list-style: none;
}

.search-result--title {
  font-size: 1rem;
  font-weight: bold;
  color: #65002a;
}

.search-result--authors {
  font-size: .9rem
}

.search-result--published {
  font-size: .8rem;
}

#all-the-books {
  width: 100%;
}

JS

new Vue({
  el: '#app',
  data() {
    return {
      searchTerm: '',
      searchResults: [],
    }
  },
  methods: {
    search() {
      axios.get(`https://www.googleapis.com/books/v1/volumes?q=` + this.searchTerm)
      .then(response => {
        this.searchResults = response.data
      })
      .catch(e => {
        console.log(e)
      })
    },
    
    bookAuthors(book) {
      let authors = book.volumeInfo.authors;
      
      if (authors.length < 3) {
        authors = authors.join(' and ')
      }
      
      else if (authors.length > 2) {
        let lastAuthor = ' and ' + authors.slice(-1);
        
        authors.pop()
        
        authors = authors.join(', ')

        authors += lastAuthor
      }
      
      return authors
    }
  }
});

6. Карточная игра

Уровень: intermediate

Описание: эта игра на время, в которой после клика по карточке, появляется изображение. Необходимо найти такую же картинку среди других карточек.

Возможности:

  • пользователь видит сетку из n x n карточек, которые изначально скрыты;
  • юзер нажимает на кнопку начала игры, и запускается таймер;
  • открытая карточка будет отображаться до тех пор, пока пользователь не нажмет на вторую;
  • если нашлись две одинаковые карточки, то они исчезают из списка, а если нет – переворачиваются в исходное положение;
  • когда нашлись все совпадения – пользователю выводится сообщение.

Бонус:

  • юзер может выбирать между несколькими уровнями сложности; увеличение сложности означает: уменьшение времени и/или увеличение количества карточек;
  • пользовательская статистика (количество выигрышей/проигрышей, лучшее время).

Пример:

HTML

<div id="g"></div>
	
<div class="logo">
  <p class="info">Click the P to get started.</p>
  <div class="card left">
    <div class="flipper">
      <div class="f c1">F</div>
      <div class="b contentbox" id="stats">
        <div class="padded">
          <h2>Figures</h2>
          Looks like you haven't FLIPped yet.
          <a href="javascript:;" class="playnow">Play now</a>
        </div>
      </div>
    </div>
  </div>
  <div class="card active twist">
    <div class="flipper">
      <div class="b f">
        <div class="c2">L</div>
      </div>
    </div>
  </div>
  <div class="card left">
    <div class="flipper">
      <div class="f c3">I</div>
      <div class="b contentbox instructions">
        <div class="padded">
          <h2>Instructions</h2>
          <p>Press [p] to pause, or [ESC] to abandon game.</p>
          <p>Flip is a timed card memory game. Click the green cards to see 
             what symbol they uncover and try to find the matching symbol underneath
             the other cards.</p>
          <p>Uncover two matching symbols at once to eliminate them from the game.</p>
          <p>Eliminate all cards as fast as you can to win the game. Have fun FLIPing!</p>
        </div>
      </div>
    </div>
  </div>
  <div class="card">
    <div class="flipper">
      <div class="f c4">P</div>
      <div class="b contentbox levels">
        <a href="javascript:;" data-level="8" class="play">Casual</a>
        <a href="javascript:;" data-level="18" class="play">Medium</a>
        <a href="javascript:;" data-level="32" class="play">Hard</a>
      </div>
    </div>
  </div>

  <p class="info">Flip should work best in Google Chrome, decent in Firefox, IE10 and Opera;</p>
</div>

CSS

body, html{margin:0;font:normal 0/0 Arial, Helvetica, sans-serif;background:#333;
   overflow:hidden;color:#fff;
	-webkit-touch-callout: none;
	-webkit-user-select: none;
	-khtml-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
}
body, html, .flipper, .f, .b, #g {width:100%;height:100%;}

/* font-face */
@font-face{font-family: "GeneralFoundicons";font-weight:normal;font-style:normal;
	src: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/60583/general_foundicons.eot");
	src: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/60583/
               general_foundicons.eot?#iefix") format("embedded-opentype"),
	     url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/60583/general_foundicons.woff") 
                format("woff"),
	     url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/60583/general_foundicons.ttf")
                format("truetype"),
	     url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/60583/general_foundicons
                .svg#GeneralFoundicons") format("svg");
}
a{text-decoration:none;color:#fff;display:block;font:bold 18px/75px
        'Open Sans';text-transform:uppercase;text-align:center;padding:0;margin:0 1px;}
a:hover{color:#fff;background:rgba(255, 255, 255, .1)}

/* FRONT SCREEN */
.logo{width:450px;height:450px;position:absolute;top:50%;left:50%;margin:-225px 0 0 -225px}
.logo .card{width:50%;height:50%;cursor:default;position:relative}
.logo .card.left.active{z-index:2}
.logo .card.left.active + .card{opacity:0}
.logo .card.left.active .b{margin-left:-100%}
.logo .f{font:normal 130px/225px 'Open Sans';text-align:center;text-transform:uppercase;}
.logo .c2{transform:scale(-1, 1);-webkit-transform:scale(-1, 1);
       -moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1)}

.logo .contentbox{width:200%;font:normal 12px/16px 'Open Sans';text-align:left}
.logo .contentbox.levels{width:100%}
.logo .contentbox .padded{padding:0 15px;}
.logo .contentbox h2{font-size:24px;padding:0;margin:28px 0;text-transform:uppercase}
.logo .contentbox p{line-height:14px;margin:14px 0}

#stats h2 i{margin-right:10px;vertical-align:top;font:italic normal 9px/8px 'Open Sans';
     color:rgba(255,255,255, .4)}
#stats ul{width:50%;display:inline-block;margin:0;padding:0;list-style-type:none;
     vertical-align:top;line-height:22px}
#stats li{display:block;margin:0;padding:0;}
#stats li b{display:inline-block;font-size:12px;width:95px}
#stats a {position:absolute;bottom:0;width:100%;margin-left:-15px;}

.logo .info{font-size:11px;line-height:16px;text-align:center;color: rgba(255,255,255,
      .3);padding:0 25px;}

/*GAME SCREEN*/
#g{margin-top:4px}
#g .timer{width:0%;height:5px;position:absolute;top:0;background:#C0392B;z-index:1}
@-webkit-keyframes timer {100% { width:100%; }}
@-moz-keyframes timer {100% { width:100%; }}
@-o-keyframes timer {100% { width:100%; }}
@keyframes timer {100% { width:100%; }}
.pause{position:absolute;width:100%;height:100%;left:0;top:0;z-index:100;
      background:rgba(51, 51, 51, 0.78)}
.pause::before{content:'PAUSED';width:100%;position:absolute;
      margin-top:-100px;top:50%;background:#C0392B;color:#fff;font:bold 80px/200px 'Open Sans';
display:block;text-align:center;}
/* card styles */
#g .found{display:inline-block}
.card{display:inline-block;-webkit-perspective:1000;-moz-perspective:1000;}
.card:not(.active):hover{opacity:.8}
.card .b::before{content:attr(data-f);display:block;font:normal 100px/100px "GeneralFoundicons";
position:absolute;top:50%;margin-top:-50px;width:100%;text-align:center}

/* flip cards */
.flipper {position:relative;-webkit-transform-style:preserve-3d;
   -moz-transform-style:preserve-3d}
.f, .b {position:absolute;top:0;left:0;box-shadow:inset 0 0 0 1px #333;
    -webkit-backface-visibility:hidden;-moz-backface-visibility:hidden}
.f {background:#16A085;z-index:1}
.b {background:#C0392B;opacity:0}
.card.active .b{z-index:2;opacity:1}

/*Grouped for easy prefixing*/
.card, .flipper, .logo .b{transition:400ms;-ms-transition:400ms;
     -webkit-transition:400ms;-moz-transition:400ms;-o-transition:400ms}
.card.active .flipper, .b {transform:rotateY(180deg);
     -webkit-transform:rotateY(180deg);-moz-transform:rotateY(180deg);
     -ms-transform:rotateY(180deg);-o-transform:rotateY(180deg)}
.logo .f, .card .b::before, .pause::before{-webkit-text-stroke: 1px #fff} /*webkit only*/

/*Responsive Stuff*/
@media all and (max-width: 1680px) and (min-width: 640px){
	.card .b::before{font-size:90px}
	#g.medium .card .b::before{font-size:70px}
	#g.hard .card .b::before{font-size:50px}
}
@media all and (max-width: 640px) and (min-width: 470px){
	.card .b::before{font-size:90px}
	#g.medium .card .b::before{font-size:60px}
	#g.hard .card .b::before{font-size:50px}
}

@media all and (max-width: 470px) and (min-width: 0px){
	.card .b::before{font-size:60px}
	#g.medium .card .b::before{font-size:35px;-webkit-text-stroke:0;}
	#g.hard .card .b::before{font-size:32px;-webkit-text-stroke:0;}
}

@media all and (max-width: 640px) and (min-width: 0px){
	a{line-height:50px;font-size:13px}
	.logo{width:300px;height:300px;margin:-150px 0 0 -150px}
	.logo .f{font:normal 80px/150px 'Open Sans'}
	.logo .contentbox h2{font-size:16px;margin:24px 0;}
	.logo .instructions h2{display:none}
	.logo .instructions.contentbox p:nth-child(3){display:none}
	#stats li b{width:auto}
}

JS

$(function(){
  
  function set(key, value) { localStorage.setItem(key, value); }
  function get(key)        { return localStorage.getItem(key); }
  function increase(el)    { set(el, parseInt( get(el) ) + 1); }
  function decrease(el)    { set(el, parseInt( get(el) ) - 1); }

  var toTime = function(nr){
    if(nr == '-:-') return nr;
    else { var n = ' '+nr/1000+' '; return n.substr(0, n.length-1)+'s'; }
  };

  function updateStats(){
    $('#stats').html('<div class="padded"><h2>Figures: <span>'+
      '<b>'+get('flip_won')+'</b><i>Won</i>'+
      '<b>'+get('flip_lost')+'</b><i>Lost</i>'+
      '<b>'+get('flip_abandoned')+'</b><i>Abandoned</i></span></h2>'+
      '<ul><li><b>Best Casual:</b> <span>'+toTime( get('flip_casual') )+'</span></li>'+
      '<li><b>Best Medium:</b> <span>'+toTime( get('flip_medium') )+'</span></li>'+
      '<li><b>Best Hard:</b> <span>'+toTime( get('flip_hard') )+'</span></li></ul>'+
      '<ul><li><b>Total Flips:</b> <span>'+parseInt( ( parseInt(get('flip_matched'))
           + parseInt(get('flip_wrong')) ) * 2)+'</span></li>'+
      '<li><b>Matched Flips:</b> <span>'+get('flip_matched')+'</span></li>'+
      '<li><b>Wrong Flips:</b> <span>'+get('flip_wrong')+'</span></li></ul></div>');
  };

  function shuffle(array) {
    var currentIndex = array.length, temporaryValue, randomIndex;
    while (0 !== currentIndex) {
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;
        temporaryValue = array[currentIndex];
        array[currentIndex] = array[randomIndex];
        array[randomIndex] = temporaryValue;
    }
    return array;
  };

  function startScreen(text){
    $('#g').removeAttr('class').empty();
    $('.logo').fadeIn(250);

    $('.c1').text(text.substring(0, 1));
    $('.c2').text(text.substring(1, 2));
    $('.c3').text(text.substring(2, 3));
    $('.c4').text(text.substring(3, 4));

    // If won game
    if(text == 'nice'){
      increase('flip_won');
      decrease('flip_abandoned');
    }

    // If lost game
    else if(text == 'fail'){
      increase('flip_lost');
      decrease('flip_abandoned');
    }

    // Update stats
    updateStats();
  };

  /* LOAD GAME ACTIONS */

  // Init localStorage
  if( !get('flip_won') && !get('flip_lost') && !get('flip_abandoned') ){
    //Overall Game stats
    set('flip_won', 0);
    set('flip_lost', 0);
    set('flip_abandoned', 0);
    //Best times
    set('flip_casual', '-:-');
    set('flip_medium', '-:-');
    set('flip_hard', '-:-');
    //Cards stats
    set('flip_matched', 0);
    set('flip_wrong', 0);
  }

  // Fill stats
  if( get('flip_won') > 0 || get('flip_lost') > 0 || get('flip_abandoned') > 0) {updateStats();}

  // Toggle start screen cards
  $('.logo .card:not(".twist")').on('click', function(e){
    $(this).toggleClass('active').siblings().not('.twist').removeClass('active');
    if( $(e.target).is('.playnow') ) { $('.logo .card').last().addClass('active'); }
  });

  // Start game
  $('.play').on('click', function(){
    increase('flip_abandoned');
		$('.info').fadeOut();

    var difficulty = '',
        timer      = 1000,
        level      = $(this).data('level');

    // Set game timer and difficulty   
    if     (level ==  8) { difficulty = 'casual'; timer *= level * 4; }
    else if(level == 18) { difficulty = 'medium'; timer *= level * 5; }
    else if(level == 32) { difficulty = 'hard';   timer *= level * 6; }	    

    $('#g').addClass(difficulty);

    $('.logo').fadeOut(250, function(){
      var startGame  = $.now(),
          obj = [];

      // Create and add shuffled cards to game
      for(i = 0; i < level; i++) { obj.push(i); }

      var shu      = shuffle( $.merge(obj, obj) ),
          cardSize = 100/Math.sqrt(shu.length);

      for(i = 0; i < shu.length; i++){
        var code = shu[i];
        if(code < 10) code = "0" + code;
        if(code == 30) code = 10;
        if(code == 31) code = 21;
        $('<div class="card" style="width:'+cardSize+'%;height:'+cardSize+'%;">'+
            '<div class="flipper"><div class="f"></div><div class="b" 
              data-f="&#xf0'+code+';"></div></div>'+
          '</div>').appendTo('#g');
      }

      // Set card actions
      $('#g .card').on({
        'mousedown' : function(){
          if($('#g').attr('data-paused') == 1) {return;}
          var data = $(this).addClass('active').find('.b').attr('data-f');

          if( $('#g').find('.card.active').length > 1){
            setTimeout(function(){
              var thisCard = $('#g .active .b[data-f='+data+']');

              if( thisCard.length > 1 ) {
                thisCard.parents('.card').toggleClass('active card found').empty(); //yey
                increase('flip_matched');

                // Win game
                if( !$('#g .card').length ){
                  var time = $.now() - startGame;
                  if( get('flip_'+difficulty) == '-:-' || get('flip_'+difficulty) > time ){
                    set('flip_'+difficulty, time); // increase best score
                  }

                  startScreen('nice');
                }
              }
              else {
                $('#g .card.active').removeClass('active'); // fail
                increase('flip_wrong');
              }
            }, 401);
          }
        }
      });

      // Add timer bar
      $('<i class="timer"></i>')
        .prependTo('#g')
        .css({
          'animation' : 'timer '+timer+'ms linear'
        })
        .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function(e) {
          startScreen('fail'); // fail game
        });

      // Set keyboard (p)ause and [esc] actions
      $(window).off().on('keyup', function(e){
        // Pause game. (p)
        if(e.keyCode == 80){
          if( $('#g').attr('data-paused') == 1 ) { //was paused, now resume
            $('#g').attr('data-paused', '0');
            $('.timer').css('animation-play-state', 'running');
            $('.pause').remove();
          }
          else {
            $('#g').attr('data-paused', '1');
            $('.timer').css('animation-play-state', 'paused');
            $('<div class="pause"></div>').appendTo('body');
          }
        }
        // Abandon game. (ESC)
        if(e.keyCode == 27){
          startScreen('flip');
          // If game was paused
          if( $('#g').attr('data-paused') == 1 ){
            $('#g').attr('data-paused', '0');
            $('.pause').remove();
          }
          $(window).off();
        }
      });
    });
  });
  
});

7. Markdown-генератор

Уровень: intermediate

Описание: данное приложение преобразует обычную таблицу с данными в форматированную Markdown-таблицу.

Возможности:

  • пользователь может создать HTML- таблицу с заданным количеством строк и столбцов;
  • юзер может вставлять текст в каждую ячейку таблицы;
  • можно генерировать Markdown-таблицу, содержащую данные из HTML-таблицы;
  • можно просматривать форматированную таблицу Markdown-таблицу.

Бонус:

  • реализовать возможность копирования форматированной таблицы в буфер обмена;
  • реализовать возможность вставить новую строку или столбец в определенное место;
  • добавить возможность полностью удалить строку или столбец.

Полезные ссылки:

Пример:

8. Линии

Данный кандидат из списка веб-проектов уже реализован много раз, но его необходимо выполнить для тренировки своих навыков.

Уровень: intermediate

Описание: приложение автоматически рисует разноцветные линии. Когда линия касается края окна, она меняет свое направление. Полосы постепенно исчезают. Библиотеки анимации не используются. Применяйте только Vanilla HTML/CSS/язык JavaScript.

Возможности:

  • линии появляются в рандомном положении в пределах границы ее окружающего окна;
  • каждые 20 мс отображается новая копия линии в новом положении на основе траектории предыдущей линии;
  • когда конечная точка линии касается границы окна, ее направление и угол меняется случайным образом;
  • после отрисовки 10-20 линий "старые" линии постепенно исчезают.

Бонус:

  • пользователь может указывать длину линии и скорость ее движения;
  • юзер имеет возможность установить появление нескольких линий и указать их траекторию и скорость.

Полезные ссылки:

Пример:

HTML

<div class="wrapper">
  <h1>Codevember 01</h1>
  <canvas id="canvas"></canvas>
</div>

CSS

$yellow: #fff275;
$pink: #f564a9;
$blue: #7ee8fa;
$purple: #3a1772;
$white: #fff;


* {
  margin: 0;
  padding: 0;
  position: relative;
}

.wrapper {
  background-image: linear-gradient(to right bottom, $yellow, $pink);
  height: 100vh;
  padding: 20px 40px;
  width: 100vw;
}

h1 {
  border-bottom: 4px solid $blue;
  color: $blue;
  display: inline-block;
  font-family: 'Monoton';
  font-weight: 400;
  font-size: 70px;
  text-shadow: -4px 4px 0 $purple,
    -8px 8px 0 $white;
  z-index: 9;
  
  &::before {
    background-color: $purple;
    content: '';
    display: block;
    height: 4px;
    position: absolute;
    top: 100%;
    transform: translateX(-4px) translateY(6px);
    width: 100%;
  }

  &::after {
    background-color: $white;
    content: '';
    display: block;
    height: 4px;
    position: absolute;
    top: 100%;
    transform: translateX(-8px) translateY(12px);
    width: 100%;
  }
}

#canvas {
  height: 100%;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;

}

JS

const canvas = document.getElementById('canvas');
const c = canvas.getContext('2d');
let lineCoords;
let lineDirs = {
  x1: 'neg',
  y1: 'neg',
  x2: 'pos',
  y2: 'pos'
}
let strokeHue = 180;
let strokeLightness = 50;
let timeoutId;
let counter = 0;
setup();
window.addEventListener('resize', setup);

function setup() {
  clearTimeout(timeoutId);
  canvas.height = canvas.clientHeight;
  canvas.width = canvas.clientWidth;
  lineCoords = {
    x1: Math.floor(Math.random() * canvas.width),
    y1: Math.floor(Math.random() * canvas.height),
    x2: Math.floor(Math.random() * canvas.width),
    y2: Math.floor(Math.random() * canvas.height)
  }
  c.lineWidth = 3;
  c.lineCap = 'round';
  draw();
}

function draw() {
  if (counter > 500) {
    c.clearRect(0, 0, canvas.width, canvas.height);  
    counter = 0;
  }
  counter++;
  c.beginPath();
  c.moveTo(lineCoords.x1, lineCoords.y1);
  c.lineTo(lineCoords.x2, lineCoords.y2);
  c.strokeStyle = `hsl(${strokeHue}, ${strokeLightness}%, 85%)`;
  strokeHue = strokeHue + 5;
  if (strokeHue > 360) {
    strokeHue = 0;
  }
  c.stroke();
  
  const x1dir = lineDirs.x1;
  const y1dir = lineDirs.y1;
  const x2dir = lineDirs.x2;
  const y2dir = lineDirs.y2;
  
  ['x1', 'y1', 'x2', 'y2'].forEach((point) => {
    if (lineDirs[point] === 'neg' && lineCoords[point] < 0) {
      lineDirs[point] = 'pos';
    } else if (point[0] === 'y' && lineCoords[point] > canvas.height) {
      lineDirs[point] = 'neg';
    } else if (point[0] === 'x' && lineCoords[point] > canvas.width) {
      lineDirs[point] = 'neg';
    }
    
    if (lineDirs[point] === 'neg') {
      lineCoords[point] = lineCoords[point] - 25;
    } else {
      lineCoords[point] = lineCoords[point] + 25;
    }
    
  });

  timeoutId = setTimeout(() => draw(), 200);
}

9. Список дел

Уровень: intermediate

Описание: классическое приложение, где пользователь может записать все мысли, задачи и цели, чтобы не забыть.

Возможности:

  • ввод задачи в специальное поле;
  • сохранение и появление в списке новой задачи по нажатию на кнопку;
  • возможность выделять выполненный пункт;
  • удаление из списка любого пункта.

Бонус:

  • возможность редактировать любой пункт;
  • просмотр выполненных и активных задач, даты их создания;
  • при закрытии окна/браузера данные должны сохраняться, а при "возвращении" пользователя динамически подгружены.

Полезные ссылки:

Пример:

HTML

<div class="tdl-holder">
 <h2>TO DO LIST</h2>
  <div class="tdl-content">
   <ul>
     <li><label><input type="checkbox"><i></i><span>get up</span><a href='#'>–</a></label></li>
     <li><label><input type="checkbox" checked><i></i><span>stand up</span><a href='#'>–</a>
          </label></li>
     <li><label><input type="checkbox"><i></i><span>don't give up the fight.</span>
        <a href='#'>–</a></label></li>
     <li><label><input type="checkbox" checked><i></i><span>save the world.</span>
           <a href='#'>–</a></label></li>
     <li><label><input type="checkbox"><i></i><span>do something else</span>
           <a href='#'>–</a></label></li>
   </ul>
   </div>
  <input type="text" class="tdl-new" placeholder="Write new item and hit 'Enter'...">
</div>

CSS

@import url(https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900);
@import url(https://fonts.googleapis.com/css?family=Roboto+Condensed:300,400,700);

body{
	color:#4a5b65;
	font-family: "Roboto", Arial, sans-serif;
	font-size:14px;
	line-height: 20px;
}

*:focus{outline:none !important;}


/* TO DO LIST
================================================== */
.tdl-holder{
	margin:0px auto;
	width: 300px;
}

.tdl-holder h2{
	background-color: #de3f53;
	border-top-left-radius: 4px;
	border-top-right-radius: 4px;
	color:#fff;
	font-family:"Roboto Condensed", Arial, sans-serif;
	font-size:16px;
	font-weight: 100;
	line-height: 56px;
	padding-left: 15px;
	margin:0;
}

.tdl-holder ul, .tdl-holder li {
	list-style: none;
	margin:0;
	padding:0;
}

.tdl-holder li{
	background-color: #262e4c;
	border-bottom:1px solid #1c2340;
	color: #b1b2c9;
}

.tdl-holder li span{
	margin-left:30px;
	-webkit-transition: all .2s linear;
	   -moz-transition: all .2s linear;
	     -o-transition: all .2s linear;
		    transition: all .2s linear;	
}

.tdl-holder label{
	cursor:pointer;
	display:block;
	line-height: 56px;
	padding: 0 15px; 
	position: relative;
}

.tdl-holder label:hover{
	background-color: #2a3353;
	color:#8284a3;
}

.tdl-holder label a{
	background-color:#de3f53;
	border-radius:50%;
	color:#fff;
	display:none;
	float:right;
	font-weight: bold;
	line-height: normal;
	height:16px;
	margin-top: 20px;
	text-align: center;
	text-decoration: none;
	width:16px;
	-webkit-transition: all .2s linear;
	   -moz-transition: all .2s linear;
	     -o-transition: all .2s linear;
		    transition: all .2s linear;	
}

.tdl-holder label:hover a{
	display: block;
}

.tdl-holder label a:hover{	
	background-color:#fff;
	color:#de3f53;
}

.tdl-holder input[type="checkbox"]{
	cursor: pointer;
	opacity: 0;
	position: absolute;
}

.tdl-holder input[type="checkbox"] + i{
	background-color: #404a6e;
	border-radius: 50%;
	display: block;
	height: 16px;
	position: absolute;
	top:20px;
	width: 16px;	
	z-index: 1;
}

.tdl-holder input[type="checkbox"]:checked + i::after{
	background-color: #6E6E96;
	border-radius: 50%;
	content: '';
	display: block;
	height:8px;
	left:4px;
	position: absolute;
	top:4px;
	width:8px;	
	z-index: 2;
}

.tdl-holder input[type="checkbox"]:checked ~ span{	
	color: #586186;
	text-decoration: line-through;
}

.tdl-holder input[type="text"]{
	background-color: #171d37;
	border: none;
	border-bottom-left-radius: 4px;
	border-bottom-right-radius: 4px;
	box-shadow: inset 0 0 8px 0 #0e1329;
	color: #464f72;
	font-size:14px;
	margin:0;
	padding:20px 15px;
	width:270px;
	-webkit-transition: all .2s linear;
	   -moz-transition: all .2s linear;
	     -o-transition: all .2s linear;
		    transition: all .2s linear;		
}

.tdl-holder input[type="text"]:hover{
	color:#4c577f;
}

.tdl-holder input[type="text"]:focus{
	color:#fff;
}

.tdl-holder ::-webkit-input-placeholder {color: #464f72;} /* WebKit browsers */
.tdl-holder :-moz-placeholder 			{color: #464f72;} /* Mozilla Firefox 4 to 18 */
.tdl-holder ::-moz-placeholder 			{color: #464f72;} /* Mozilla Firefox 19+ */
.tdl-holder :-ms-input-placeholder 		{color: #464f72;} /* Internet Explorer 10+ */

.tdl-holder li.remove{
	-webkit-animation:collapseItem 300ms ease;
			animation:collapseItem 300ms ease;
	-webkit-transform-origin: 50% 0%;
		-ms-transform-origin: 50% 0%;
			transform-origin: 50% 0%;
}

.tdl-holder li.remove span{
	color: #586186;
	text-decoration: line-through;
}

@keyframes collapseItem {
    0% 		{ -ms-transform: perspective(500px) rotateX(0deg);transform: perspective
        (500px) rotateX(0deg); 	}
    100% 	{ -ms-transform: perspective(500px) rotateX(-90deg);transform: perspective
        (500px) rotateX(-90deg); }
}

@-webkit-keyframes collapseItem {
    0% 		{ -webkit-transform: perspective(500px) rotateX(0deg); 	}
    100% 	{ -webkit-transform: perspective(500px) rotateX(-90deg);}
}

JS

  /* TO DO LIST */
  $(".tdl-new").bind('keypress', function(e){
    var code = (e.keyCode ? e.keyCode : e.which);
    if(code == 13) {
      var v = $(this).val();
      var s = v.replace(/ +?/g, '');
      if (s == ""){
        return false;
      }else{
        $(".tdl-content ul").append("<li><label><input type='checkbox'><i></i><span>"+ v 
                +"</span><a href='#'>–</a></label></li>");
        $(this).val("");
      }
    }
  });

  $(".tdl-content a").bind("click", function(){
    var _li = $(this).parent().parent("li");
        _li.addClass("remove").stop().delay(100).slideUp("fast", function(){
          _li.remove();
        });
    return false;
  });

  // for dynamically created a tags
  $(".tdl-content").on('click', "a", function(){
    var _li = $(this).parent().parent("li");
        _li.addClass("remove").stop().delay(100).slideUp("fast", function(){
          _li.remove();
        });
    return false;
  });

10. Морской бой

Уровень: advanced

Описание: текстовый вариант графической версии игры. Игра состоит из движка (BGE) и консольной части.

Возможности:

BGE

  • для запуска используется функция startGame(), чтобы начать игру с 1 игроком: функция создаст игровую доску 8x8, состоящую из 3 кораблей, размещенных рандомно;
  • для выстрела используется функция shoot(), передающая координаты пересечения строки и столбца на игровом поле. Функция вернет информацию о статусе (попадание или промах), количестве оставшихся кораблей, а также массивы размещения кораблей, попаданий и промахов.

Консольная часть

  • пользователь может видеть массив выстрелов, отображаемый в двумерном виде, возвращаемый функцией startGame();
  • юзеру предлагается ввести координаты целевой ячейки;
  • все попадания и промахи, а также сообщения о результате выстрела и поздравление отображаются после выстрела;
  • в конце игры предлагается повторить раунд.

Полезные ссылки:

Пример:

11. Чат

Это приложение самое интересное из подборки веб-проектов и самое популярное на данные момент.

Уровень: advanced

Описание: классический мессенджер.

Возможности:

  • пользователю предлагается ввести свой ник при входе (он будет сохранен в приложении);
  • присутствует поле для ввода сообщения и кнопка отправки. Отправленное сообщение появится в окне чата рядом с ником.

Бонус:

  • сообщения должны быть видны всем пользователям чата (WebSockets);
  • когда новый пользователь присоединяется к чату, всем юзерам приходит сообщение;
  • сообщения хранятся в БД;
  • в чат можно посылать смайлы, изображения, видео и ссылки;
  • есть возможность создать приватную комнату.

Полезные ссылки:

Пример:

HTML

<div class="wrapper"></div>

<input class="message" type="text" placeholder="Enter message and press enter"/>

<div class="initModal">
	<h3>What's your name?</h3>
	<input type="text" class="username" placeholder="Enter your name and press enter"/>
</div>

CSS

//usual reset stuff
*,*:before,*:after,ul,li,a,button,input,h1,h2,h3,h4,h5,h6,p, img, image, svg, path, g  {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
	background-color: transparent;
	border: none;
	text-decoration: none;
	font-family: 'Roboto';
	user-select: none;
	list-style: none;
}
$swipes-shadow-one: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.12);
$swipes-shadow-two: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
$swipes-shadow-three: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
$swipes-shadow-four: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
$swipes-shadow-five: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);

html, body {
	width: 100%;
	height: 100%;
	margin: 0;
	padding: 0;
	background-color: #eee;
	font-family: 'Roboto';
}

li {
	opacity: 0;
	animation: fadeIn .4s ease-in-out forwards;
}

@keyframes fadeIn {
	100% {
		opacity: 1;
	}
}
.wrapper {
	width: 100%;
	max-width: 650px;
	margin: 0 auto;
	padding-bottom: 70px;
	overflow-x: hidden;
	word-break: break-all;
}

li {
	padding: 10px 20px;
	color: #34495e;
	word-break: break-all;
	
	span {
		color: #e74c3c;
		font-weight: 700;
	}
}

input.message {
	width: 100%;
	max-width: 650px;
	//border: 1px solid #333;
	position: fixed;
	bottom: 0;
	height: 60px;
	background-color: #fff;
	box-shadow: $swipes-shadow-one;
	padding: 0 20px;
	left: 50%;
	transform: translateX(-50%);
	
	&:focus {
		outline: none;
	}
}

.initModal {
	width: 100%;
	height: 100%;
	position: fixed;
	left: 0;
	top: 0;
	background-color: #fff;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-direction: column;
	
	h3 {
		font-weight: 300;
		color: #777;
	}
	
	input.username {
		width: 350px;
		height: 60px;
		background-color: #eee;
		padding: 0 15px;
		margin-top: 25px;
		color: #444;
		
		&:focus {
			outline: none;
		}
	}
}

JS

//If you fork this, please change this database link to your own.
var fb = new Firebase("https://codepentestt.firebaseio.com/");
var messages = fb.child("messages");
var btn = $('button');
var wrap = $('.wrapper');
var input = $('input.message');
var usernameInput = $('input.username');

var user = [];

(function($) {
	$.sanitize = function(input) {
		var output = input.replace(/<script[^>]*?>.*?<\/script>/gi, '').
					 replace(/<[\/\!]*?[^<>]*?>/gi, '').
					 replace(/<style[^>]*?>.*?<\/style>/gi, '').
					 replace(/<![\s\S]*?--[ \t\n\r]*>/gi, '');
	    return output;
	};
})(jQuery);

usernameInput.on('keyup', function(e) {
	if (e.keyCode === 13 && usernameInput.val().length > 0) {
		var getTxt = usernameInput.val();
		user.push(getTxt);
		usernameInput.val('');
		$('.initModal').css('display', 'none');
		console.log(user);
	}
});

input.on('keyup', function(e) {
	var curUsername = user.join();
	if (e.keyCode === 13 && input.val().length > 0) {
		var getTxt = input.val();
		messages.push({
			user: curUsername,
			message: getTxt
		});
		input.val('');
	}
});

messages.limitToLast(100).on("child_added", function(snap) {
	wrap.append('<li><span>' + $.sanitize(snap.val().user) + ':</span> ' + $.sanitize(snap.val().message) + '</li>');
	window.scrollTo(0,document.body.scrollHeight);
});

Следующий в списке веб-проектов – классная полезность для Git-а. Зацените!

12. GitHub Timeline

Уровень: advanced

Описание: приложение должно показывать всю историю создания репозиториев, их имена и даты создания по конкретному юзеру. График можно расшарить. Используются только публичные репозитории.

Возможности:

  • ввод имени пользователя GitHub;
  • по нажатию на кнопку генерируется график активности данного пользователя;
  • если такого юзера не существует – выводится сообщение об ошибке.

Бонус:

  • возможность просматривать репозитории по году создания.

Полезные ссылки:

Пример:

HTML

<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,
600italic,700,700italic' rel='stylesheet' type='text/css'>
  
<!-- The Timeline -->

<ul class="timeline">

	<!-- Item 1 -->
	<li>
		<div class="direction-r">
			<div class="flag-wrapper">
				<span class="flag">Freelancer</span>
				<span class="time-wrapper"><span class="time">2013 - present
                               </span></span>
			</div>
			<div class="desc">My current employment. Way better than the 
                             position before!</div>
		</div>
	</li>
  
	<!-- Item 2 -->
	<li>
		<div class="direction-l">
			<div class="flag-wrapper">
				<span class="flag">Apple Inc.</span>
				<span class="time-wrapper"><span class="time">
                                     2011 - 2013</span></span>
			</div>
			<div class="desc">My first employer. All the stuff 
                          I've learned and projects I've been working on.</div>
		</div>
	</li>

	<!-- Item 3 -->
	<li>
		<div class="direction-r">
			<div class="flag-wrapper">
				<span class="flag">Harvard University</span>
				<span class="time-wrapper"><span class="time">
                                     2008 - 2011</span></span>
			</div>
			<div class="desc">A description of all the lectures 
                                 and courses I have taken and my final degree?</div>
		</div>
	</li>
</ul>

CSS

body {	
  margin: 0;
  padding: 0;
  background: rgb(230,230,230);
  
  color: rgb(50,50,50);
  font-family: 'Open Sans', sans-serif;
  font-size: 112.5%;
  line-height: 1.6em;
}

/* ================ The Timeline ================ */

.timeline {
  position: relative;
  width: 660px;
  margin: 0 auto;
  margin-top: 20px;
  padding: 1em 0;
  list-style-type: none;
}

.timeline:before {
  position: absolute;
  left: 50%;
  top: 0;
  content: ' ';
  display: block;
  width: 6px;
  height: 100%;
  margin-left: -3px;
  background: rgb(80,80,80);
  background: -moz-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, 
rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,
rgba(30,87,153,1)), color-stop(100%,rgba(125,185,232,1)));
  background: -webkit-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, 
rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
  background: -o-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, 
rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
  background: -ms-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, 
rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
  background: linear-gradient(to bottom, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, 
rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
  
  z-index: 5;
}

.timeline li {
  padding: 1em 0;
}

.timeline li:after {
  content: "";
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
}

.direction-l {
  position: relative;
  width: 300px;
  float: left;
  text-align: right;
}

.direction-r {
  position: relative;
  width: 300px;
  float: right;
}

.flag-wrapper {
  position: relative;
  display: inline-block;
  
  text-align: center;
}

.flag {
  position: relative;
  display: inline;
  background: rgb(248,248,248);
  padding: 6px 10px;
  border-radius: 5px;
  
  font-weight: 600;
  text-align: left;
}

.direction-l .flag {
  -webkit-box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
  -moz-box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
  box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
}

.direction-r .flag {
  -webkit-box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
  -moz-box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
  box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
}

.direction-l .flag:before,
.direction-r .flag:before {
  position: absolute;
  top: 50%;
  right: -40px;
  content: ' ';
  display: block;
  width: 12px;
  height: 12px;
  margin-top: -10px;
  background: #fff;
  border-radius: 10px;
  border: 4px solid rgb(255,80,80);
  z-index: 10;
}

.direction-r .flag:before {
  left: -40px;
}

.direction-l .flag:after {
  content: "";
  position: absolute;
  left: 100%;
  top: 50%;
  height: 0;
  width: 0;
  margin-top: -8px;
  border: solid transparent;
  border-left-color: rgb(248,248,248);
  border-width: 8px;
  pointer-events: none;
}

.direction-r .flag:after {
  content: "";
  position: absolute;
  right: 100%;
  top: 50%;
  height: 0;
  width: 0;
  margin-top: -8px;
  border: solid transparent;
  border-right-color: rgb(248,248,248);
  border-width: 8px;
  pointer-events: none;
}

.time-wrapper {
  display: inline;
  
  line-height: 1em;
  font-size: 0.66666em;
  color: rgb(250,80,80);
  vertical-align: middle;
}

.direction-l .time-wrapper {
  float: left;
}

.direction-r .time-wrapper {
  float: right;
}

.time {
  display: inline-block;
  padding: 4px 6px;
  background: rgb(248,248,248);
}

.desc {
  margin: 1em 0.75em 0 0;
  
  font-size: 0.77777em;
  font-style: italic;
  line-height: 1.5em;
}

.direction-r .desc {
  margin: 1em 0 0 0.75em;
}

/* ================ Timeline Media Queries ================ */

@media screen and (max-width: 660px) {

.timeline {
 	width: 100%;
	padding: 4em 0 1em 0;
}

.timeline li {
	padding: 2em 0;
}

.direction-l,
.direction-r {
	float: none;
	width: 100%;

	text-align: center;
}

.flag-wrapper {
	text-align: center;
}

.flag {
	background: rgb(255,255,255);
	z-index: 15;
}

.direction-l .flag:before,
.direction-r .flag:before {
  position: absolute;
  top: -30px;
	left: 50%;
	content: ' ';
	display: block;
	width: 12px;
	height: 12px;
	margin-left: -9px;
	background: #fff;
	border-radius: 10px;
	border: 4px solid rgb(255,80,80);
	z-index: 10;
}

.direction-l .flag:after,
.direction-r .flag:after {
	content: "";
	position: absolute;
	left: 50%;
	top: -8px;
	height: 0;
	width: 0;
	margin-left: -8px;
	border: solid transparent;
	border-bottom-color: rgb(255,255,255);
	border-width: 8px;
	pointer-events: none;
}

.time-wrapper {
	display: block;
	position: relative;
	margin: 4px 0 0 0;
	z-index: 14;
}

.direction-l .time-wrapper {
	float: none;
}

.direction-r .time-wrapper {
	float: none;
}

.desc {
	position: relative;
	margin: 1em 0 0 0;
	padding: 1em;
	background: rgb(245,245,245);
	-webkit-box-shadow: 0 0 1px rgba(0,0,0,0.20);
	-moz-box-shadow: 0 0 1px rgba(0,0,0,0.20);
	box-shadow: 0 0 1px rgba(0,0,0,0.20);
	
  z-index: 15;
}

.direction-l .desc,
.direction-r .desc {
	position: relative;
	margin: 1em 1em 0 1em;
	padding: 1em;
	
  z-index: 15;
}

}

@media screen and (min-width: 400px ?? max-width: 660px) {

.direction-l .desc,
.direction-r .desc {
	margin: 1em 4em 0 4em;
}

}

13. Spell-It

Уровень: advanced

Описание: приложение Spell-It помогает пользователям практиковать свое правописание, воспроизводя слова в аудиозаписи, которые пользователь затем должен написать. Применяется углубленное программирование JavaScript.

Возможности:

  • по нажатию на кнопку "Play" воспроизводится слово, которое должно быть введено;
  • кнопка "Enter" отправляет введенное слово на проверку;
  • при правильном/неправильном вводе слова появляется сообщение об ошибке или успехе.

Бонус:

  • реализовать звуковое оповещение, когда слово правильно написано;
  • реализовать подачу предупреждающего сигнала при неправильном написании;
  • создать кнопку, "подчеркивающую" ошибки в слове.

Полезные ссылки:

Пример:

Оригинал

Понравилось? Вас также заинтересуют:

Какой из этих веб-проектов вы уже написали или планируете написать?

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Frontend разработчик (react native)
по итогам собеседования
Unity Tech Lead
по итогам собеседования
Ведущий программист Unity3D
Москва, по итогам собеседования
Middle\Senior .Net разработчик
от 120000 RUB до 165000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG