An Instagram clone built with Vue.js and CSSGram

Instagram (with Vue.js)

An Instagram clone built with Vue.js and CSSGram. Upload photos, pick filters, and like posts!

html

<div id="app">
  <div class="app__phone">
    <div class="phone-header">
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Instagram_logo.svg/500px-Instagram_logo.svg.png" />
      <p class="cancel-cta" v-if="step === 2 || step === 3" @click="goToHome">Cancel</p>
      <p class="next-cta" v-if="step === 2" @click="step++">Next</p>
      <p class="next-cta" v-if="step === 3" @click="sharePost">Share</p>
    </div>
    <transition name="fade">
      <div class="feed" v-if="step === 1" v-dragscroll.y="true">
        <instagram-post v-for="post in posts"
                        :post="post"
                        :key="posts.indexOf(post)">
        </instagram-post>
      </div>
    </transition>
    <div v-if="step === 2">
      <div class="selected-image"
           :class="filterType"
           :style="{ backgroundImage: 'url(' + image + ')' }"></div>
      <div class="filter-container" v-dragscroll.x="true">
        <filter-type v-for="filter in filters"
                     :filter="filter"
                     :image="image"
                     :key="filter.name">
        </filter-type>
      </div>
    </div>
    <div v-if="step === 3">
      <div class="selected-image"
           :class="filterType"
           :style="{ backgroundImage: 'url(' + image + ')' }"></div>
      <div class="caption-container">
        <textarea class="caption-input"
                  placeholder="Write a caption..."
                  type="text"
                  v-model="caption">
        </textarea>
      </div>
    </div>
    <div class="phone-footer">
      <div class="home-cta" @click="goToHome">
        <i class="fas fa-home fa-lg"></i>
      </div>
      <div class="upload-cta">
        <input type="file"
               name="file"
               id="file"
               class="inputfile"
               @change="fileUpload"
               v-model="fileInput"
               :disabled="step !== 1"/>
        <label for="file">
          <i class="far fa-plus-square fa-lg"></i>
        </label>
        <p v-if="step === 1">
          Click <a @click="uploadRandomImage">here for a random image!</a> or upload your own! <i class="fas fa-chevron-right"></i>
        </p>
      </div>
    </div>
  </div>
  <div class="details">
    <a class="button is-primary is-small is-info" v-if="!showDetails" @click="showDetails = !showDetails">Details</a>
    <ul v-else>
      <li>Navigate the feed by <span>dragging (or scrolling)</span></li>
      <li>Upload an image with <span><i class="far fa-plus-square fa-lg"></i></span></li>
      <li>Like a post with <span><i class="far fa-heart fa-lg"></i></span> or <span>double clicking an image</span></li>
    </ul>
  </div>
  <a href="https://twitter.com/djirdehh" target="_blank" class="twitter-section">
    <i class="fab fa-twitter" aria-hidden="true"></i>
  <a>
</div>

<!--  Prefetch random images -->
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/tropical_beach.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/downtown.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/cat.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/sushi.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/pug_personal.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/pineapple.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/tropical_ocean.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/velvet_monkey.jpg" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/codepen_logo.png" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/me2.png" />
<link rel="prefetch" href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/me_3.jpg" />

Css

$small: 520px;
$medium: 768px;
$large: 1216px;

@import url('https://fonts.googleapis.com/css?family=Roboto:400,700');

html, body, #app {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: #e6ecf1;
  font-family: 'Roboto', sans-serif;
}

#app {
  display: flex;
  align-items: center;
  justify-content: center;
}

.app__phone {
  background-color: white;
  height: 620px;
  width: 375px;
  overflow: hidden;
}

.phone-header {
  height: 50px;
  width: 375px;
  position: sticky;
  position: -webkit-sticky;
  top: 0;
  background: #fafafa;
  border-bottom: 1px solid #eeeeee;
  z-index: 99;

  img {
    max-width: 100px;
    display: block;
    margin: 0 auto;
    padding-top: 10px;
  }
  
  .cancel-cta,
  .next-cta {
    position: absolute;
    top: 12px;
    color: #209cee;
    font-weight: bold;
    cursor: pointer;
  }

  .cancel-cta {
    left: 10px;
  }

  .next-cta {
    right: 10px;
  }
}

.feed {
  height: 100%;
  overflow-y: scroll;
  overflow-x: hidden;
  margin-right: -15px;
}

.instagram-post {
  padding-top: 50px;
}

.instagram-post ~ .instagram-post {
  padding-top: 0;
}

.instagram-post {
  padding: 5px 0;
  .header {
    height: 30px;
    border-bottom: 1px solid #fff;
    margin: 7.5px 10px;
    .image {
      display: inline-block;
    }
    img {
      border-radius: 99px;
    }
    .username {
      padding-left: 5px;
      font-size: 0.90rem;
      font-weight: bold;
    }
  }
  .image-container {
    height: 330px;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: cover;
  }
  .content {
    margin: 7.5px 10px;
  }
  .far.fa-heart,
  .fas.fa-heart{
    cursor: pointer;
  }
  .fas.fa-heart {
    color: #f06595;
  }
  .likes {
    margin: 5px 0;
    margin-bottom: 5px !important;
    font-size: 0.85rem;
    font-weight: bold;
  }
  .caption {
    font-size: 0.85rem;
    span {
      font-weight: bold;
    }
  }
}

.instagram-post:last-child {
  margin-bottom: 80px;
}

.selected-image {
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center center;
  height: 330px;
}

.filter-container {
  height: 210px; 
  padding: 30px 10px;
  white-space: nowrap;
  overflow-x: hidden;
}

.filter-type {
  width: 100px;
  display: inline-block;
  margin: 0 3px;
  p {
    font-size: 11px;
    text-align: center;
    text-transform: capitalize;
    padding-bottom: 5px;
  }
  .img {
    cursor: pointer;
    width: 100px;
    height: 100px;
    background-size: cover;
    background-position: center center;
  }
}

.filter-type:last-child {
  margin-right: 20px;  
}

.caption-container {
  height: 210px;
  display: flex;
  align-items: center;
  justify-content: center;
  
  textarea {
    border: 0;
    font-size: 1.0rem;
    width: 100%;
    padding: 10px;
    border-bottom: 1px solid #eeeeee;
  }
  
  textarea:focus {
    outline: 0;
  }
}

.phone-footer {
  height: 35px;
  width: 375px;
  position: sticky;
  position: -webkit-sticky;
  bottom: 0;
  background: #fafafa;
  border-top: 1px solid #eeeeee;
  z-index: 99;
  
  .home-cta {
    position: absolute;
    left: 10px;
    top: 6px;
    cursor: pointer;
  }
  
  .upload-cta {
    position: absolute;
    right: 10px;
    top: 6px;
    p {
      font-size: 0.63rem;
      position: absolute;
      left: -25px;
      top: 5px;
    }
  }

  input[name="file"] {
    visibility: hidden;
  }
  
  label {
    cursor: pointer;
    z-index: 99;
  }
}

.details {
  position: absolute;
  left: 10px;
  bottom: 10px;
  li {
    font-size: 0.8rem;
    span {
      font-weight: bold;
    }
  }
}

.twitter-section {
  position: absolute;
  right: 10px;
  bottom: 10px;
  .fa-twitter {
    color: #209cee;
    font-size: 1.0rem;
    &:hover {
      color: #1496ed;
    }
  }
}

.fade-leave-active {
  transition: opacity .5s
}
.fade-leave-to {
  opacity: 0
}

// Media Queries
@media(max-width: $small) {
  #app {
    height: 100% !important;
    padding-top: 0 !important;
  }

  .app__phone,
  .app__phone__scroll__cover {
    height: 100%;
    width: 100%;
  }

  .phone-header,
  .phone-footer {
    width: 100%;
  }
}

@media(max-width: $small) {
  .details {
    display: none;
  }
}

@media(max-width: $large) and (max-height: $medium) {
  #app {
    height: initial;
    padding-top: 5px;
  }
}

JavaScript

const EventBus = new Vue();

const posts = [
   {
    username: 'socleansofreshh',
    userImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/me_3.jpg',
    postImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/tropical_beach.jpg',
    likes: 36,
    upVoted: false,
    caption: "When you're too ready for summer '18 ☀️",
    filter: 'perpetua'
   },
   {
    username: 'djirdehh',
    userImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/me2.png',
    postImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/downtown.jpg',
    likes: 20,
    upVoted: false,
    caption: 'Views from the six...',
    filter: 'clarendon'
   },
   {
    username: 'puppers',
    userImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/pug_personal.jpg',
    postImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/puppers.jpg',
    likes: 49,
    upVoted: false,
    caption: 'Current mood ?',
    filter: 'lofi'
  }
]

const filters = [
  { name: 'normal' }, { name: 'clarendon' }, { name: 'gingham' }, { name: 'moon' }, { name: 'lark' }, { name: 'reyes' }, { name: 'juno' }, { name: 'slumber' }, { name: 'aden' }, { name: 'perpetua' }, { name: 'mayfair' }, { name: 'rise' }, { name: 'hudson' }, { name: 'valencia' }, { name: 'xpro2' }, { name: 'willow' }, { name: 'lofi' }, { name: 'inkwell' }, { name: 'nashville' }
]

Vue.component('instagram-post', {
  template: 
  `
    <div class="instagram-post">
      <div class="header level">
          <div class="level-left">
            <figure class="image is-32x32">
              <img :src="post.userImage" />
            </figure>
            <span class="username">{{post.username}}</span>
          </div>
      </div>
      <div class="image-container"
           :class="post.filter"
           :style="{ backgroundImage: 'url(' + post.postImage + ')' }"
           @dblclick="like">
      </div>
      <div class="content">
        <div class="heart">
          <i class="far fa-heart fa-lg"
            :class="{'fas': !this.post.upVoted,  'fas': this.post.upVoted}"
            @click="like">
          </i>
        </div>
        <p class="likes">{{post.likes}} likes</p>
        <p class="caption"><span>{{post.username}}</span> {{post.caption}}</p>
      </div>
    </div>
  `,
  props: ['post'],
  methods: {
    like() {
      this.post.upVoted ? this.post.likes-- : this.post.likes++;
      this.post.upVoted = !this.post.upVoted;
    }
  }
});

Vue.component('filter-type', {
  template:
  `
   <div class="filter-type">
     <p>{{filter.name}}</p>
    <div class="img"
         :class="filter.name"
         :style="{ backgroundImage: 'url(' + image + ')' }"
         @click="selectFilter">
    </div> 
   </div>
  `,
  props: ['filter', 'image'],
  methods: {
    selectFilter() {
      EventBus.$emit('selectFilter', {filter: this.filter.name});
    }
  }
});

new Vue({
  el: "#app",
  data: {
    posts,
    image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/downtown.jpg',
    caption: '',
    filterType: 'normal',
    step: 1,
    showDetails: false,
    fileInput: ''
  },
  created () {
    EventBus.$on('selectFilter', (evt) => {
      this.filterType = evt.filter;
    })
  },
  methods: {
    fileUpload(e) {
      const files = e.target.files || e.dataTransfer.files;
      if (!files.length) return;
      this.image = files[0];
      this.createImage();
    },
    createImage() {
      const image = new Image();
      const reader = new FileReader();

      reader.onload = e => {
        this.image = e.target.result;
        this.step = 2;
      };
      reader.readAsDataURL(this.image);
    },
    uploadRandomImage() {
      const randomImages = [
        'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/cat.jpg',
        'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/sushi.jpg',
        'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/velvet_monkey.jpg',
        'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/pineapple.jpg',
        'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/tropical_ocean.jpg'
      ];
      
      this.image = randomImages[Math.floor(Math.random() * randomImages.length)];
      this.step = 2;
    },
    goToHome() {
      this.image = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/downtown.jpg';
      this.caption = '';
      this.filterType = 'normal';
      this.step = 1;
    },
    sharePost() {
      const post = {
        username: 'codepen',
        userImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/codepen_logo.png',
        postImage: this.image,
        likes: 0,
        caption: this.caption,
        filter: this.filterType
      }
      
      this.posts.unshift(post);
      this.goToHome();
    }
  }
});

Author

Hassan Dj

Demo

See the Pen Instagram (with Vue.js) by Hassan Dj (@itslit) on CodePen.