勉強履歴

プログラミング初心者のメモ書きです

なぜこのポートフォリオを作ろうとしているのか

またしばらくの間更新をサボってしまってましたが、 現在もエンジニア転職を目指してポートフォリオを作成している途中です。
作ろうとしているアプリは以下のアプリです。

github.com

アカウントのIDを入力して診断ボタンを押すと、 普段のツイートに応じた診断結果がドラゴンの形で帰ってきて、 そのアカウントの人となりをある程度知ることができるアプリです。
READMEにも書いてありますが、このアプリを作ろうとしたより詳細な理由をまとめておきたいと思います。

何故このアプリを作ろうとしているのか

  • SNS(Twitter)を使い始めたが、匿名である以上攻撃的、乃至読んだ人に負の影響を与える内容の発信はどうしてもある
  • そのような発言を見るとモヤモヤする
  • 他人がそのような発言をすることはもちろんの事、自分自身も行っている可能性がある
  • 予め相手がどんな人間かある程度分かれば、SNS上だけに限定されるが、付き合い方の準備が出来る
  • 自分自身が周囲に与える影響も客観的に捉えることができて、SNSの使い方を改めるきっかけになる

何で診断結果を🐉にしたか

  • 自分が好きなだけ
  • 元々作ろうとしてたアプリを軌道修正した結果
    • ペルソナ診断を作ろうとしてた。
      ここで言うペルソナとは、ゲームペルソナシリーズに出てくる超能力のことで、この能力を持つ者は心の中にある自分自身を神話上 に 出てくる神や悪魔の形で召喚出来る。
      また、ペルソナはタロットカードの大アルカナで分類分けされており、心理テストを回答していき、その診断結果を神や悪魔で表現する予定だった。
  • ただ、以下の理由から挫折。
    • 自身がいまいち版権もので作るのに抵抗があった。
    • 一般的に神話とかタロットカードについて知ってる人は少なく、ゲームを知らない人には分かりにくいのではと指摘される。
    • 診断結果にイラストをつけたいと思ったが、公式の画像を使うわけにはいかないし、ココナラとかで頼んだとしてもかなりの数のイ    ラストになるため、結構な額が必要となることが予想される。
  • ペルソナの中には一部🐉も含まれている。
    • 神話上の神や悪魔は、人間が作り出したものである以上、完璧な存在ではなく、どこか人間を反映した要素を持っており、それは🐉に も言える。
    • 🐉は西洋圏では悪の象徴とみなされる事が多い一方で、東洋圏では恐れの対象として神扱いされている。
    • 善悪関係なく強さの象徴として描かれる事が多く、一般の認知度が高い上にどちらかと言えばプラスのイメージがついている印象を受 けるため、診断の結果として表示するのにいいのではないかと思った。
    • 後イラストお願いするのであれば、自分が好きなものの方がいい。
  • ただ、ドラゴンの診断アプリは既に類似品が結構あったので、一捻り加える必要があった。
    • そこで、過去のスクール生のアプリを見ていたら、テキストの感情を分析して数値化するAPIを使って、その結果を診断の判定に使って いる方がいて、感情分析API面白そうだなと思って使おうとした結果、今の形に不時着した(ここまで来るのに4〜5ヶ月)。

類似アプリとの差別点(エンジニアチェッカーやきのこネガティブ診断)

まだ開発途中なので、現段階での話になりますが‥

1. 技術的な点

  • Vue
  • TailwindCSS

2. ユーザー視点

  • ユーザー登録機能で、診断結果を追うことができる(予定)
  • エンジニアチェッカーはインフルエンサーを撲滅する目的で作られた(基本は他の人が対象)。
    きのこは自分のSNSでの発言がネガティブになってないかをチェックするという自己管理の目的で作られた(基本は自分が対象)?
    このアプリは周囲に悪影響を及ぼしがちな人(悪いドラゴン)から自身の身を守りつつ、自分が周囲に悪影響を及ぼさないようにする(邪竜化)しないようにする目的なので、対象は自他両方。

エンジニアのアカウント診断 | エンジニアチェッカー

あなたのツイートのネガティブ度を診断します! -きのこネガティブ診断-

まとめ

とりあえずですがまとめてみました。
ポートフォリオ用のアプリとしてどうなんだという気持ちも正直あります(二番煎じ、三番煎じ感が‥)。
元々業務改善系のアプリ作ろうとしていましたが、現実問題として厳しいことを知り、
今度は身近な課題解決系のアプリにしようとしましたが、どうしてもネガティブな感じになってしまってました。
だったら自分の趣味に走ってしまえという方向性で行ったらここに行き着いてしまった感じです。
完全オリジナルなんて今時無理でしょうし、初めて挑むことだからうまく行かなくて当然なので、ダメ元で取り敢えず完成させたいと思います。

Tailwind Cssの導入方法

久しぶりのブログ投稿になってしまいました。 現在、ポートフォリオを鋭意制作中です。

タイトルにもあるように、ここ数日は Tailwind Cssポートフォリオで使おうとインストールしてました。 忘備録として残しておきます。

1. yarnでインストールする

CDNで取り込むやnpmでインストールする方法もありますが、 今回はyarnで行いました。

yarn add tailwindcss@latest postcss@latest autoprefixer@latest

2. Tailwind Cssの設定を行う

今回はapp/javascriptディレクトリの下にcssディレクトリを作って、 そこにtailwind.config.jsを置くことにしました。

cd app/javascript/css
yarn tailwindcss init

作成されたtailwind.config.js

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

3.postcss.config.jsの設定変更

ホームディレクトリにあるpostcss.config.jsに以下の記述を加えます。

module.exports = {
  plugins: [
    require('tailwindcss')('./app/javascript/css/tailwind.config.js'),
    require('autoprefixer'),
  ]
}

4.tailwind.cssを作成してビルド

cssディレクトリ内にtailwind.cssを作成して次のように記載しました。

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

その後、次のコマンドでビルドしました。

yarn tailwind build ./app/javascript/css/tailwindcss.css ./app/javascript/css/tailwindcss_dev.css

長いので、package.jsonのscriptを利用することでコマンドを省略することができます。

  "scripts": {
    "dev-css": "tailwind build ./app/javascript/css/tailwindcss.css ./app/javascript/css/tailwindcss_dev.css"
  }


私の場合はビルドしようとした時にpostcss plugin autoprefixer requires postcss 8というエラーが出てしまいました。 エラーが出た時点でのpackage.json

"dependencies": {
    "autoprefixer": "^10.3.1",
    "postcss": "^8.3.5",
    "tailwindcss": "^2.2.4",
  },

postcss8は入っているはずなのですがエラーが出てしまったので、tailwindcssも含めて互換性のあるバージョンに落としました。

yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

5. 実際に画面を表示させる

次のファイルにそれぞれ以下の記述を追記することでTailwind Cssが使えるようになります。 app/javascript/packs/application.js

 import '../css/tailwind.css';

app/views/layouts/application.html.erb

<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>

後は、自分の表示させたいページのclassにUtilityクラスを書いていけばOKです。 実際にページを実装していくことを考えると、tailwind.config.jsの中身を色々と設定していく必要がありますが、 長くなりそうなので今回のメモはここまでにします。

ちなみにTailwind Cssを使ってみようとした理由は以下の3つです。 ①好奇心 ②流行りらしいから ③名前が自分の作ろうとしているポートフォリオのイメージと合っている(?)

参考にしたサイト

tailwindcss.com

tailwindcss.com

Rails6でTailwind CSSを使ってみる

Vue.js学習メモ(検索機能の実装)

Vue.jsを使って、リアルタイムで検索結果を表示する検索フォームを実装することを考えます。

検索フォームに打ち込んだ文字列をタイトルに含むタスクのみが画面上に表示されるようにします。

task/index.vue

<template>
<label for="search">絞り込み</label>
  <input
    id="search"
    v-model="searchTask"  // dataに定義したsearchTaskに入力した文字列を入れ込む
    type="text"
    placeholder="タスク名を入力してください"
    class="form-control"
  >
</template>

<script>
export default {
  data() {
    return {
      searchTask: '',
    }
  },
  computed: {
    todoTasks() {
      return this.filteredTasks.filter(task => {
        // 下に定義したfilteredTasksで絞り込んだtodoタスクのみ表示
        return task.status == "todo"
      })
    },
    filteredTasks() {
      return this.tasks.filter(task => {
        return task.title.indexOf(this.searchTask) != -1  
        // indexOf関数で-1にならない=searchTaskに代入されている文字列を含む
      })
    }
  },
  .
  .
  .
</script>

土日と平日の学習モチベーションに大きな差が生じている現状をどうにかしたい今日この頃です。

Vue.js学習メモ(画像アップロード)

ログイン機能を実装しているタスク管理アプリで、 個々のユーザーがプロフィール画像を登録できるように、画像アップロード機能を実装することを考えます。
RailsのActiveStorageを使って画像登録します。

①userモデルにプロフィール画像用のカラム(avatar)を設定

user.rb

class User< ApplicationRecord

  has_one_attached :avatar

end

②ユーザーに紐づくプロフィール画像のurlを返すメソッドをモデルに

class User< ApplicationRecord

  def avatar_url
    avatar.attached? ? Rails.application.routes.url_helpers.rails_blob_path(avatar, only_path: true) : 
nil
end

③プロフィールのコントローラーとルーティングを設定

api/profile_controller.rb

class Api::ProfileController < ApplicationController
  before_action :authenticate!

  def update
    user = User.find(current_user.id)
    if user.update(user_params)
      render json: user, methods: [:avatar_url]
    else
      render json: user.errors, status: :bad_request
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :avatar)
  end
end
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resource :profile
  end
end

④userコントローラーにapiのエンドポイントとストロングパラメータを追加

api/users_controller.rb

class Api::UsersController < ApplicationController

  def me
    render json: current_user, methods: [:avatar_url]
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name, :avatar)
  end
end

⑤storeにユーザー更新のAPIリクエストを送る処理を記載

const actions = {
  .
  .
  .
  updateUser({ commit, state }, user) {
    return axios.patch(`profile/${state.authUser.id}`, user)
      .then(res => {
        commit('setUser', res.data)
      })
  }
}

⑥プロフィールページの作成

profile/index.vue

<template>
  <div id="profile-form">
    <ValidationObserver v-slot="{ handleSubmit }">
      <div class="form-group text-left">
        <ValidationProvider v-slot="{ errors }" rules="required">
          <label for="name">ユーザー名</label>
          <input 
            id="name" 
            v-model="user.name" 
            type="text" 
            class="form-control" 
            placeholder="username"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <ValidationProvider v-slot="{ errors }" ref="provider" rules="image">
          <label for="avatar" class="d-block >プロフィール画像</label>
          <img :src="user.avatar_url" class="my-3" width="150px">
          <input
            id="avatar"
            type="file"
            accept="image/png,image/jpeg"
            class="form-control-file"
            @change="handleChange"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <button
        type="submit"
        class="btn btn-primary"
        @click="handleSubmit(update)"
      >
        更新
      </button>
    </ValidationObserver>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'ProfileIndex',
  data() {
    return {
      user: {
        name: "",
        avatar_url: ""
      },
      uploadAvatar: ""
    }
  },
  computed: {
    ...mapGetters('users', ['authUser'])
  },
  created() {
    this.user = Object.assign({}, this.authUser)
  },
  methods: {
    ...mapActions('users', ['updateUser']),
    async handleChange(event) {
      const { valid } = await this.$refs.provider.validate(event)
      if (valid) this.uploadAvatar = event.target.files[0]
    },
    update() {
      const formData = new FormData()
      formData.append("user[name]", this.user.name)
      if (this.uploadAvatar) formData.append("user[avatar]", this.uploadAvatar)

      try {
        this.updateUser(formData)
        this.$router.push({ name: "TaskIndex" })
      } catch (error) {
        console.log(error);
      }
    },
  }
}
</script>

<style scoped>
</style>

⑦プロフィールページのルーティング

router/index.js

import ProfileIndex from "../pages/profile/index"

const router = new Router({
  routes: [
    .
    .
    .
    {
      path: '/profile',
      component: ProfileIndex,
      name: 'ProfileIndex',
      meta: { requiredAuth: true },
    }
  ]
})

⑧ヘッダーにプロフィール画像の表示+プロフィールページへのリンクを設定

components/TheHeader.vue

<li class="nav-item active avatar-image-wrapper">
  <img
    :src="authUser.avatar_url"
    class="rounded avatar-image"
  >
</li>
<li class="nav-item active">
  <router-link
    :to="{ name: 'ProfileIndex' }"
    class="nav-link"
  >
    プロフィール
  </router-link>
</li>

ハイライトが上手く表示されてないので、次の土日に直します(直した)。

Vue.js学習メモ(バリデーション)

VeeValidateを使って、タスク追加・編集フォームのバリデーションチェックを行っていきます。 フォームに入力されたデータに対して、リアルタイムでエラーを表示させることができるようになります。

①インストール

yarn add vee-validate
// もしくは
npm install vee-validate

②バリデーションルールを設定したいフォルダのscriptに書く。  長くなったり、複数のフォルダで設定したい場合は、専用のフォルダに書く。

import Vue from 'vue'

//  vee-validateのライブラリを呼び出し
import {
  ValidationProvider,
  ValidationObserver,
  extend
} from 'vee-validate'

//  バリデーションルール
import {
  email,
  required
} from 'vee-validate/dist/rules';

// Vue.componentに上記で取得したValidationProviderとValidationObserverを読み込ませる
Vue.component('ValidationObserver', ValidationObserver) 
//  各フィールドを監視するために使用
Vue.component('ValidationProvider', ValidationProvider) 
//  フォーム全体を監視するために使用

// 以下、extendでルールやメッセージの定義(emailなどはあらかじめ組み込まれている)
extend('email', {
  ...email,
  message: '{_field_}の形式で入力してください'
});

extend('required', {
  ...required,
  message: '{_field_}は必須項目です'
});

extend('min', {
  validate(value, { length }) {
    return value.length >= length;
  },
  params: ['length'],
  message: '{_field_}は{length}文字以上で入力してください'
});

③定義したルールをフォームで使う(例としてログインフォーム)。

<template>
  <div id="login-form">
    <ValidationObserver v-slot="{  handleSubmit  }">  
       //  ValidationObserverで全体を監視
      <div class="form-group text-left">
        <ValidationProvider  
          //  ValidationProviderで個別のフォームを監視
          v-slot="{ errors }"  
          // ValidationProvider内のエラーが全てそのまま配列に入ってる
          rules="required|email"  
          //  ここでは、必須とemailの形式になっているかのバリデーションルール
        >
          <label for="email">メールアドレス</label>
          <input
            id="email"
            v-model="user.email"
            name="メールアドレス"
            type="email"
            class="form-control"
            placeholder="test@example.com"
          >
          <span class="text-danger">{{ errors[0] }}</span>  
         //  ここでエラーの表示
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <ValidationProvider
          v-slot="{ errors }"
          rules="required|min:3"  
          //  ここでは、必須チェック及び最低文字数が3文字以上か
        >
          <label for="password">パスワード</label>
          <input
            id="password"
            v-model="user.password"
            name="パスワード"
            type="password"
            class="form-control"
            placeholder="password"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <button
        type="submit"
        @click="handleSubmit(login)"
      >
        ログイン
      </button>
    </ValidationObserver>
  </div>
</template>

一回、最近書いた記事を見直した方がいいかなと思います(主に見やすさや出典の表記)。 そろそろ、vueを使ってアプリを作ろうかと思います。

Vue.js学習メモ(コンポーネントの分割)

Vue.jsとrailsでタスク管理アプリを作りながら勉強してます。 今回はタスクを3つ(TODO、DOING、DONE)に分けて、 なおかつタスクの一覧(TaskList)とタスク単体(TaskItem)を別のコンポーネントに分割していきたいと思います。 コンポーネント間の関係:index(親)→TaskList(子)→TaskItem(孫)

 変更前

index.vue

<div class="h4">TODO</div>
  <div
    v-for="task in tasks"
    :key="task.id"
    :id="'task-' + task.id"
    class="bg-white border shadow-sm rounded my-2 p-4 d-flex align-items-center"
    @click="handleShowTaskDetailModal(task)"
  >
    <span>{{ task.title }}</span>
</div>

 変更後

index.vue(TODOのみ)

<TaskList
  :tasks="todoTasks"  
  // v-bindでTaskListにtasksプロパティを与える。todoTasksはcomputedで定義。
  taskListId="todo-list"
  @handleShowTaskDetailModal="handleShowTaskDetailModal"
>
  <template v-slot:header>  
   // slotでは親で差し替えたい部分の表記を書く
    <div class="h4">TODO</div> 
  </template>
</TaskList>

 computed: {
   todoTasks() {
     return this.tasks.filter(task => {
       return task.status == "todo"
     })
  },
}

TaskList.vue

<div :id="taskListId" class="bg-light rounded shadow m-3 p-3">
  <slot name="header">タスク区分</slot>  
  //子では<slot></slot>と書くだけ
  <template v-for="task in tasks">  
  //v-forでtasks内のtaskを表示していく
    <TaskItem :key="task.id" :task="task" @handleShowTaskDetailModal="$listeners['handleShowTaskDetailModal']" /> 
    // taskItemにtaskプロパティを渡す
    // $listenersで全てのtaskItem(孫)からのデータを受け取り、index(親)に渡す(Vue3では$attrsに統合)
  </template>
</div>

TaskItem.vue

<div
  :id="'task-' + task.id"
  class="bg-white border shadow-sm rounded my-2 p-4 d-flex align-items-center"
  @click="handleShowTaskDetailModal(task)"
>
  <span>{{ task.title }}</span>
</div>

methods: {
  handleShowTaskDetailModal(task) {
    this.$emit('handleShowTaskDetailModal', task)
  }
}

今回はなかなかどう分割したらいいのかがわからず、上手いこと行きませんでした。 まだまだvueの理解が不十分ですね。勉強を継続していきたいと思います。

土日も書くと言っておきながら結局やってませんでした。 いきなり週3でブログ更新は荷が重かったのかもしれないので、最低でも週1にハードル下げてやっていきます。

Vue.js学習メモ(子から親への値受け渡し)

昨日の続きです。

モーダルから新しくタスクを追加する場合を考えます。 子コンポーネント(TaskCreateModal.vue)→親コンポーネント(index.vue) 引き渡すデータ:タスクの情報(タイトル/title、詳細/description)

実装例

index.vue

<template> 
  <transition name="fade">
    <TaskCreateModal 
      v-if="isVisibleTaskCreateModal" 
      @close-modal="handleCloseTaskCreateModal" 
      @create-task="handleCreateTask"
    />
  </transition>
</template>

<script>
import TaskCreateModal from "./components/TaskDetailModal"
export default {
  name: "TaskIndex",
  components: {
    TaskCreateModal
  },
  methods: {
  handleCreateTask(task) {
  // 略
  }
}

TaskCreateModal.vue

<template>
  <div class="modal-body">
    <div class="form-group">
      <label for="title">タイトル</label>
      <input type="text" class="form-control" id="title" v-model="task.title">
    </div>
    <div class="form-group">
      <label for="description">説明文</label>
      <textarea rows="5" class="form-control" id="description" v-model="task.description"></textarea>
    </div>
    <div class="d-flex justify-content-between">
      <button @click="handleCreateTask" class="btn btn-success">追加</button>
    </div>
  </div>
</template>

<script>
export default {
  name: "TaskCreateModal",
  methods: {
    handleCreateTask() {
      this.$emit('create-task', this.task)
    },
  },
}
</script>

1)子コンポーネントをインポートし、componentsプロパティに登録(親から子のときと同じ)

import TaskCreateModal from "./components/TaskCreateModal"
export default {
  name: "TaskIndex",
  components: {
    TaskCreateModal
  },

2)v-modelディレクティブを使って、送信するデータを設定
タイトルに入力した値がtask.titleに、説明文に入力した値がtask.descriptionに代入される

<div class="form-group">
  <label for="title">タイトル</label>
  <input type="text" class="form-control" id="title" v-model="task.title">
</div>
<div class="form-group">
  <label for="description">説明文</label>
  <textarea rows="5" class="form-control" id="description" v-model="task.description"></textarea>
</div>

3)クリックイベントの設定
@click="処理名"とタグに記載することで、クリックした時にmethodsプロパティで定義した処理(今回はhandleCreateTask)が実行される

<template>
  <div class="d-flex justify-content-between">
    <button @click="handleCreateTask" class="btn btn-success">追加</button>
  </div>
</template>

<script>
export default {
   methods: {
    handleCreateTask() {
      this.$emit('create-task', this.task)
    }
}
</script>

4)子コンポーネントから親へのデータ送信
this.$emit('イベント名', 値)と記載することで、第二引数で値を渡しつつ親のイベントを引き起こす(今回は'create-task')

<script>
export default {
   methods: {
    handleCreateTask() {
      this.$emit('create-task', this.task)
    }
}
</script>

5)親コンポーネントでデータを受けとり(@create-task)、handleCreateTaskが実行される

<template>
  <transition name="fade">
    <TaskCreateModal 
      v-if="isVisibleTaskCreateModal" 
      @close-modal="handleCloseTaskCreateModal" 
      @create-task="handleCreateTask"
    />
  </transition>
</template>

<script>
export default {
  methods: {
    handleCreateTask(task) {
    // 略
  }
}
</script>

以上です。 あくまで自分用のメモではありますが、他の人が見てもわかりやすいブログに改善していきたいですね。