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

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


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

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

ТОП-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" отправляет введенное слово на проверку;
  • при правильном/неправильном вводе слова появляется сообщение об ошибке или успехе.

Бонус:

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

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

Пример:

Оригинал

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

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

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

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