tag:crieit.net,2005:https://crieit.net/users/ckoshien/feed ckoshienの投稿 - Crieit Crieitでユーザーckoshienによる最近の投稿 2020-12-30T20:52:17+09:00 https://crieit.net/users/ckoshien/feed tag:crieit.net,2005:PublicArticle/16448 2020-12-30T20:51:19+09:00 2020-12-30T20:52:17+09:00 https://crieit.net/posts/f05ede37f52343628fe7705f6a531ed7 キャップ野球専用スコアブック入力アプリをリリースした <h1 id="蓋野球スコア入力アプリ「CAP-SCOREBOOK」"><a href="#%E8%93%8B%E9%87%8E%E7%90%83%E3%82%B9%E3%82%B3%E3%82%A2%E5%85%A5%E5%8A%9B%E3%82%A2%E3%83%97%E3%83%AA%E3%80%8CCAP-SCOREBOOK%E3%80%8D">蓋野球スコア入力アプリ「CAP-SCOREBOOK」</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://cap-scoresheet.netlify.app">https://cap-scoresheet.netlify.app</a></p> <h2 id="操作マニュアル"><a href="#%E6%93%8D%E4%BD%9C%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB">操作マニュアル</a></h2> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/XtF5HmEmwWU" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h2 id="機能概要"><a href="#%E6%A9%9F%E8%83%BD%E6%A6%82%E8%A6%81">機能概要</a></h2> <ul> <li>非ログイン時(閲覧モード) <ul> <li>試合結果閲覧</li> <li>試合内容閲覧</li> <li>個人成績閲覧</li> <li>チーム成績閲覧</li> <li>リーグ成績閲覧</li> <li>ログイン時(入力モード)</li> <li>チーム新規作成</li> <li>選手新規作成</li> <li>試合新規作成</li> <li>スターティングメンバー設定</li> <li>結果入力 <ul> <li>1球ごと入力</li> <li>1打席ごと入力</li> </ul></li> <li>自責点・勝敗S入力</li> </ul></li> </ul> <h2 id="開発環境"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83">開発環境</a></h2> <ul> <li>NextJS(static export)</li> <li>Netlify</li> <li>検証機: Pixel4a(Android)/12mini(iOS)/PC</li> <li>サーバ側: NodeJS</li> <li>DB: MySQL</li> <li>Docker-compose</li> <li>ReactNative For Web</li> <li>ユーザ認証: firebase</li> </ul> <h2 id="開発の経緯"><a href="#%E9%96%8B%E7%99%BA%E3%81%AE%E7%B5%8C%E7%B7%AF">開発の経緯</a></h2> <p>10/21 蓋世・エスト監督からスコアを共有できるアプリはないか聞かれる<br /> 10/25 1打席ごとの入力ができるWEBアプリの仮実装が終わる<br /> 10/30 スコアの活用方法思い付かずに実装中断<br /> 11/26<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/Yokoppe_cap/status/1331653356872298496">https://twitter.com/Yokoppe_cap/status/1331653356872298496</a><br /> よこっぺさんの一言で中断してたスコアアプリの開発のやる気が復活する。<br /> 11/28 リーグ戦の帰りにkamiさんが「投球分析したいなぁ」と言う話をする。<br /> 11/29〜<br /> ある程度実装が終わっていたアプリに1球ごとのコース分析機能を拡張したアプリに着手。<br /> 当初はReactNativeを使ったandroid/iOSアプリを想定。<br /> 12/1 ネイティブアプリから得意なWEBアプリへの舵の切り直しを宣言。<br /> 12/4 ReactNative For Webの採用を決定。<br /> 12/7 Hirooookiさんから投手視点への切り替えが欲しいとの要望を受けるhttps://twitter.com/gstiltonhs/status/1335606102675783680<br /> 12/11 配球分析機能の実装が完了<br /> 12/14 いらすとやからのデザイン変更<br /> 12/21 テストコードの実装に着手<br /> 12/30 α版リリース</p> <h2 id="今回新たに使った技術"><a href="#%E4%BB%8A%E5%9B%9E%E6%96%B0%E3%81%9F%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">今回新たに使った技術</a></h2> <ul> <li>ReactNative For Web</li> </ul> <p>CSSが書きにくいと思って敬遠していたのですが、いざ使ってみるとモバイルに最適化されていて、きちんとスタイルを書けば複雑なUIも実現できたので、食わず嫌いはよくないなと。</p> <h2 id="開発する上で役に立ったこと"><a href="#%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B%E4%B8%8A%E3%81%A7%E5%BD%B9%E3%81%AB%E7%AB%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">開発する上で役に立ったこと</a></h2> <p>やっぱり<a target="_blank" rel="nofollow noopener" href="https://jcbl-score.com">みんなのSCORE</a>の開発経験ですかね。<br /> DB設計的にはCAP-SCOREBOOKの方が1球ずつ記録する分複雑なのですが、<br /> CAP-SCOREBOOKにほとんどDB設計を応用できました。</p> <h1 id="今後の展開(β版へ向けて)"><a href="#%E4%BB%8A%E5%BE%8C%E3%81%AE%E5%B1%95%E9%96%8B%28%CE%B2%E7%89%88%E3%81%B8%E5%90%91%E3%81%91%E3%81%A6%29">今後の展開(β版へ向けて)</a></h1> <ul> <li>共同編集機能</li> <li>スコアシート出力機能(需要があれば)</li> </ul> ckoshien tag:crieit.net,2005:PublicArticle/16063 2020-09-20T00:57:20+09:00 2020-09-20T11:34:07+09:00 https://crieit.net/posts/9607755c4800bec007d1a912ce8f42dc 草野球のライブ配信を補助するツールを作った話 <h1 id="草野球放送局"><a href="#%E8%8D%89%E9%87%8E%E7%90%83%E6%94%BE%E9%80%81%E5%B1%80">草野球放送局</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://baseball-broadcast.vercel.app/">https://baseball-broadcast.vercel.app/</a></p> <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f66295a77955.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f66295a77955.jpg?mw=700" alt="" /></a></p> <h1 id="仕組み"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF">仕組み</a></h1> <p>「ツイキャスゲームズ」などのスクリーン配信に対応したサービスで、<br /> スマホの画面を直接ストリーミング配信できることを利用して、<br /> スマホ側でカメラを起動して画面を合成したらいいじゃないかという発想に。<br /> react-webcamというライブラリ(canvasで描画している)とNextJSを組み合わせました。</p> <h1 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h1> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/EjPa1nqVwAk" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h1 id="使った技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">使った技術</a></h1> <h2 id="NextJS"><a href="#NextJS">NextJS</a></h2> <p>最低でもQA画面と設定画面、合成画面が必要になると思っていたので、<br /> NextJSを選択しました。</p> <h2 id="Vercel"><a href="#Vercel">Vercel</a></h2> <p>デプロイ用のプラットフォーム</p> <h2 id="react-webcam"><a href="#react-webcam">react-webcam</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/mozmorris/react-webcam">react-webcam</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ozota/items/0fc56f600955c6380dde">react(nativeでない)でカメラを使ってみる</a></li> </ul> <h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1> <h2 id="カメラのデバッグ"><a href="#%E3%82%AB%E3%83%A1%E3%83%A9%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0">カメラのデバッグ</a></h2> <p>セキュリティ的にカメラの起動条件がlocalhostか、https通信下だったので、最初はvercelに上げながら動作確認していましたが、途中からcodesandboxを使っていました。</p> <p><strong>iPhone/Chromeではカメラが起動しない</strong>とか。</p> <h2 id="スマホの向きを変えたときの描画"><a href="#%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%AE%E5%90%91%E3%81%8D%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E6%8F%8F%E7%94%BB">スマホの向きを変えたときの描画</a></h2> <p>縦横の向きを変えたときにリサイズする処理を色々試したのですが、</p> <ul> <li>addEventListener('resize')</li> <li>再描画回避のためにuseRefを使う</li> <li>縦横のサイズを入れ替える</li> </ul> <p>どれも結局上手くいかず、stackoverflowなどを見ながら最終的にたどり着いたのはCSSでの表示制御でした。えぇぇぇぇぇ.....。</p> <h2 id="UI詰め過ぎ問題"><a href="#UI%E8%A9%B0%E3%82%81%E9%81%8E%E3%81%8E%E5%95%8F%E9%A1%8C">UI詰め過ぎ問題</a></h2> <p>スコアボードのUIを詰め過ぎてタップしづらいという問題が発生。<br /> <code>transform: scale(1.3)</code>でコンポーネントごと1.3倍にして逃げました。<br /> <a href="https://crieit.net/boards/web1week-202005/WEB-UI#パーツ%28コンポーネント%29の組み合わせ">秘伝のたれを思いついた瞬間</a></p> <pre><code class="javascript">import Webcam from "react-webcam"; import NHKBoard from "./NHKBoard"; const Home = () => { const videoConstraints = { facingMode: { exact: "environment" }, aspectRatio: 1.78 }; return ( <div style=<span>{</span><span>{</span> width:'95vw', height:'95vh', position:'relative' <span>}</span><span>}</span> > <Webcam style=<span>{</span><span>{</span> position:'absolute', top:0, left:0, width:'95vw', height:'95vh', <span>}</span><span>}</span> videoConstraints={videoConstraints} /> <div style=<span>{</span><span>{</span> transform:'scale(1.3)', position:'absolute', bottom:0, right:0 <span>}</span><span>}</span> > <NHKBoard/> </div> </div> ) } export default Home; </code></pre> <h1 id="IKEN"><a href="#IKEN">IKEN</a></h1> <p>スコアボードの種類を選べるようにするとか、選手紹介を表示するとか機能拡張は考えていますが、一旦最小構成でリリースしますのでフィードバックいただけるとうれしいです!</p> <p><a target="_blank" rel="nofollow noopener" href="https://ikens.net/ckoshien_tech/baseball-broadcast">https://ikens.net/ckoshien_tech/baseball-broadcast</a></p> ckoshien tag:crieit.net,2005:PublicArticle/16045 2020-09-01T23:08:37+09:00 2020-09-01T23:08:37+09:00 https://crieit.net/posts/A-Frame-VR-5f4e55e505ae0 A-FrameでVR選手ロッカーを作ってみた話 <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f4e4eb3ed4c2.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f4e4eb3ed4c2.jpg?mw=700" alt="" /></a></p> <h2 id="①NextJSでaframe-reactを動かす"><a href="#%E2%91%A0NextJS%E3%81%A7aframe-react%E3%82%92%E5%8B%95%E3%81%8B%E3%81%99">①NextJSでaframe-reactを動かす</a></h2> <h3 id="aframe-react"><a href="#aframe-react">aframe-react</a></h3> <p>ReactJSでAFrameを動かすときはこれ。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/supermedium/aframe-react">aframe-react</a></p> <p>NextJSは一部SSR(サーバサイドレンダリング)が走るので、クライアントで動作する状態になってからロードする必要がある。<br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://github.com/michaltakac/aframe-next-static/blob/master/pages/index.js">aframe-next-static</a></p> <p>windowというグローバル変数が使えるようになればクライアント側で動作する状態なので、レンダリング完了フラグで管理する。</p> <pre><code class="javascript">if (typeof window !== "undefined") { require("aframe"); setRendered(true); } </code></pre> <h2 id="②aframe-reactで書いてみる"><a href="#%E2%91%A1aframe-react%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">②aframe-reactで書いてみる</a></h2> <p>aframeは配置するものが多いと意外とソースが長くなるので、<br /> 1個のLockerを構成する単位でコンポーネント化しました。<br /> 選手の情報をAPIから取得して、奇数と偶数の場合で振り分けています。</p> <pre><code class="html">if (!rendered || !teamMembers) { return <></>; } return ( <Scene //device-orientation-permission-ui="enabled: true" > {teamMembers.map((member,idx)=>( <> {idx%2 === 0 ?<RightLocker member={member} x={3 - idx*0.35} y={0} z={1.7+idx*0.35} rot_x={0} rot_y={0} rot_z={0}/> :<LeftLocker member={member} x={-idx*0.35} y={0} z={idx*0.35} rot_x={0} rot_y={0} rot_z={0}/> } </> ))} <Entity primitive="a-sky" material="color: #555" /> <Entity camera look-controls wasd-controls position="3 1 2"/> </Scene> ); </code></pre> <p><code>a-sky</code>は背景、<code>camera</code>はカメラの初期位置を定義しています。</p> <p>これがRightLockerのソースです。<br /> プログラム的に難しいことはないのですが、<br /> 座標や光源の調整などが結構面倒でした。<br /> (..planeって光透過すんの?)</p> <pre><code class="javascript">import { Entity } from "aframe-react"; import { Player } from "../../model/typedef"; type diff={ x:number, y:number, z:number, rot_x:number, rot_y:number, rot_z:number, member:Player } const RightLocker:React.FC<diff> = ({x,y,z,rot_x,rot_y,rot_z,member}) => { return ( <> <Entity geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: "/images/wood.jpg",alphaTest:0.1 <span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.7, y: y, z: z-4.3<span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: -135+rot_y, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-image" height="2" //geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: "/images/wood.jpg", transparent:false<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.7, y: y, z: z-5 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: rot_y-45, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-image" height="2" //geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: "/images/wood.jpg"<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x, y: y, z: z-4.3 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: rot_y-45, z: rot_z <span>}</span><span>}</span> /> <Entity geometry=<span>{</span><span>{</span> primitive: "box", width:0.5, height:0.3<span>}</span><span>}</span> material=<span>{</span><span>{</span> color:'gray' <span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.5, y: y-1, z: z-4.5 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: rot_y-45, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-image" height="1.5" width="0.8" //geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: member.ogp_image<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.7, y: y, z: z-4.3<span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: -135+rot_y, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-light" light=<span>{</span><span>{</span>type:"spot"<span>}</span><span>}</span> material=<span>{</span><span>{</span>color:'yellow'<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.3, y: y+1.3, z: z-4.5 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x-90, y: rot_y-10, z: rot_z-10 <span>}</span><span>}</span> /> </> ); }; export default RightLocker; </code></pre> <h2 id="DEMO"><a href="#DEMO">DEMO</a></h2> <p>まだ本サイトにはデプロイしていませんが、<code>team/[チームID]/vr_locker</code>で各チームのVRロッカーを見ることができます。<br /> - <a target="_blank" rel="nofollow noopener" href="https://deploy-preview-19--cap-baseball-info.netlify.app/team/67/vr_locker">キャップ野球情報局・VRロッカー</a></p> ckoshien tag:crieit.net,2005:PublicArticle/16034 2020-08-18T23:18:26+09:00 2020-08-19T20:34:19+09:00 https://crieit.net/posts/Crieit-5f3be332191fb 【アップデート】Crieitバッジを作りました <h1 id="Crieitバッジ"><a href="#Crieit%E3%83%90%E3%83%83%E3%82%B8">Crieitバッジ</a></h1> <p>GitHubなどに貼るCrieitバッジを作りました。</p> <p>これが私(ckoshien)のバッジです。<br /> <a target="_blank" rel="nofollow noopener" href="http://crieit.net/users/ckoshien"><img src="https://ogp-vercel.vercel.app/crieit/ckoshien" alt="Crieit経験値" /></a></p> <p>ちなみにCrieit開発者だらさんはといえば....<br /> <a target="_blank" rel="nofollow noopener" href="http://crieit.net/users/dala00"><img src="https://ogp-vercel.vercel.app/crieit/dala00" alt="Crieit経験値" /></a></p> <p>....やっぱり強い強すぎる。</p> <h2 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h2> <p>このコードの*****の部分を自分のユーザID(例:ckoshienとかdala00)に書き換えて貼るだけです。</p> <pre><code>[![Crieit経験値](https://ogp-vercel.vercel.app/crieit/*****)](http://crieit.net/users/*****) </code></pre> <h1 id="どうやって作ったか"><a href="#%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%8B">どうやって作ったか</a></h1> <h2 id="着想"><a href="#%E7%9D%80%E6%83%B3">着想</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mikkame/mikkame">mikkame</a>さんの<a target="_blank" rel="nofollow noopener" href="https://qiita-badge.apiapi.app/">Qiita のスコアをGithub風バッジに変換するサービス</a>を見てCrieit版作ろう!と思いました。</p> <h2 id="技術的なこと"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E3%81%93%E3%81%A8">技術的なこと</a></h2> <p>shield.io版からアップデートしました。<br /> 現在はvercel + NextJS + puppeteerでスクレイピングした情報で一旦画面を作ってスクリーンショットを撮っています。</p> <p>結果的にvercelを2台分(スクレイピング/スクリーンショット)使っています。</p> <h3 id="バックエンド"><a href="#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89">バックエンド</a></h3> <ul> <li>vercel/NodeJS(lambda)</li> <li>puppeteer</li> </ul> <p>puppeteerでcrieitのユーザページをスクレイピングして経験値を取得しています。</p> <pre><code class="javascript">async function getCrieitBadge(user_id) { const browser = await puppeteer.launch({ args: chrome.args, executablePath: await chrome.executablePath, headless: chrome.headless, }); const page = await browser.newPage(); await page.goto("https://crieit.net/users/"+user_id); await page.setCacheEnabled(true); const itemSelector = "#app > #container > div.row > div > p.mb-2 > span:nth-child(1)"; const itemSelector2 = "#app > #container > div.row > div > p.mb-2 > span:nth-child(2)"; var item = await page.$(itemSelector); var item2 = await page.$(itemSelector2); var data = await (await item.getProperty('textContent')).jsonValue(); var data2 = await (await item2.getProperty('textContent')).jsonValue(); await page.close(); await browser.close(); return data; } </code></pre> <p>現在は画像を1日キャッシュするようにして返しています。<br /> ではよいCrieitライフを!!</p> ckoshien tag:crieit.net,2005:PublicArticle/15925 2020-06-07T23:44:07+09:00 2020-06-07T23:44:07+09:00 https://crieit.net/posts/NextJS-5edcfd37d54af NextJSでマークダウンエディタを扱う際の注意点 <h2 id="SSR(サーバサイドレンダリング)の影響"><a href="#SSR%28%E3%82%B5%E3%83%BC%E3%83%90%E3%82%B5%E3%82%A4%E3%83%89%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%29%E3%81%AE%E5%BD%B1%E9%9F%BF">SSR(サーバサイドレンダリング)の影響</a></h2> <p>draft.jsやtui-editorに共通して言えることですが、<br /> 元々ブラウザでの動作を想定して作られているので、<br /> NextJSなどのSSR環境下では<code>window</code>、<code>document</code>などの変数の中身が<code>undefined</code>になり、動作しなくなります。</p> <h2 id="Next.JSではSSRを無効にできる"><a href="#Next.JS%E3%81%A7%E3%81%AFSSR%E3%82%92%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%A7%E3%81%8D%E3%82%8B">Next.JSではSSRを無効にできる</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr">With no SSR</a></p> <p>インポート方法を変えることでSSRを無効にできるオプションがあります。<br /> dynamic import自体はES2020で導入される構文だとか。</p> <pre><code class="typescript">const DraftEditorNoSSR = dynamic<DraftEditorProp>( () => import('../components/DraftEditor') as any, { ssr: false } ) </code></pre> <h2 id="Draft.jsを断念した理由"><a href="#Draft.js%E3%82%92%E6%96%AD%E5%BF%B5%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1">Draft.jsを断念した理由</a></h2> <p>draft.jsでは再描画した際にカーソルが行の先頭に戻るというバグがあります。<br /> 公式リポジトリでは行の最後に移動する対処も議論されていますが、文章の中で編集したくなったらどうするんだ....という声もちらほら。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/facebook/draft-js/issues/1198">How to stop DraftJS cursor jumping to beginning of text?</a></p> <p>さっさと見切りをつけてToastUI editorに移りました。<br /> そうです、crieitにも採用されているエディタですね。</p> <h2 id="Toast UI Editorで実装できた!"><a href="#Toast+UI+Editor%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%A7%E3%81%8D%E3%81%9F%21">Toast UI Editorで実装できた!</a></h2> <p>ただし、Toast UI Editorはrefを使わないと値が取れず、<br /> マウントされていない状態でrefがnullになってしまうことにかなり苦しめられました。</p> <p>changeイベントがeditorで発火するのでフラグを更新するようにして、外側でrefを使ってデータを取り出すという実装になりました。<br /> それから、画像アップロードをdisabledにしました。</p> <p>editorコンポーネントを使いまわしたかったので、<br /> 表示用の初期値<code>initialValue</code>と値を更新して外部に渡す<code>setFunc</code>をpropsとして与えています。</p> <pre><code class="javascript">const DraftEditor = ({ initialValue, setFunc }) => { const editorRef = createRef<Editor>(); const [changed, setChanged] = useState(false); useEffect(() => { if (editorRef.current && changed) { setFunc(editorRef.current.getInstance().getMarkdown()); console.log("set"); setChanged(false); } }, [editorRef.current, changed]); return ( <div style=<span>{</span><span>{</span> width: "100vw", <span>}</span><span>}</span> > <div> <Editor previewStyle="vertical" toolbarItems={[ "heading", "bold", "italic", "strike", "divider", "hr", "quote", "ul", "ol", "task", "table", "link", "divider", ]} height="400px" initialEditType="markdown" initialValue={initialValue} ref={editorRef} hideModeSwitch={true} events=<span>{</span><span>{</span> change: (e) => { setChanged(true); }, <span>}</span><span>}</span> /> </div> </div> ); }; export default DraftEditor; export type DraftEditorProp = { initialValue: string; setFunc: Dispatch<any>; }; </code></pre> ckoshien tag:crieit.net,2005:PublicArticle/15899 2020-05-16T23:29:13+09:00 2020-05-16T23:29:13+09:00 https://crieit.net/posts/NextJS アニメのレコメンドサービス、NextJS化しました。 <p>先週<a href="https://crieit.net/posts/b3c547c34df6393a1f86f8072aaf510a">アニメのレコメンドサービス</a>を作ったばかりなのですが、<br /> 業務で<strong>Next.js</strong>と<strong>React hooks</strong>が必要になり、<br /> 勉強がてらソースコードの少ないものをリメイクしてみました。</p> <h1 id="サービスのURL"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AEURL">サービスのURL</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://annict-suggest.netlify.app/">Annictさじぇすと!</a></p> <h1 id="Next.js"><a href="#Next.js">Next.js</a></h1> <p>Next.jsはReactJSを用いたフレームワークです。<br /> <a target="_blank" rel="nofollow noopener" href="https://bagelee.com/programming/next-js/setup-next-js/">Next.jsの環境構築【これからはじめるNext.js】</a></p> <p>Next.jsのメリットとしては読み込むファイルが軽量になった分パフォーマンスが向上することでしょうか。</p> <h2 id="環境構築"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89">環境構築</a></h2> <p>今回環境構築の参考にしたのは<a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/typescript-guide/reactenv.html">こちら</a></p> <pre><code>(プロジェクトルート)  ├ components (コンポーネント類)  ├ context (React contextを格納するフォルダ)  ├ hooks (React hooksを格納するフォルダ)  ├ lib  │ └ gtag.js (analytics)  ├ pages  │ ├ _app.tsx (レイアウト)  │ ├ _document.tsx (index.htmlに相当)  │ ├ index.tsx  │ └ works  │   └ [id].tsx (idは可変なのでdynamic routes)  ├ public  │ └ images  └ styles (CSS) </code></pre> <h2 id="pages"><a href="#pages">pages</a></h2> <p>ファイルシステムの構造がそのままWEBシステムのパスになります。<br /> 例外として、<code>_app.tsx</code>と<code>_document.tsx</code>があります。</p> <h3 id="_app.tsx"><a href="#_app.tsx">_app.tsx</a></h3> <p>レイアウトを担当。</p> <pre><code class="js">const App = ({Component,pageProps}:AppProps) => { Router.events.on('routeChangeComplete', url => gtag.pageview(url)); return( <storeContext.Provider value={useSeasonName()}> <Layout> <Component {...pageProps} /> </Layout> </storeContext.Provider> ); } </code></pre> <h3 id="_document.tsx"><a href="#_document.tsx">_document.tsx</a></h3> <p>いわゆるindex.htmlに相当。<br /> あれ、これclass構文だったわ。(コピペ)</p> <pre><code class="js">class MyDocument extends Document<Props> { render() { return( <Html lang="ja-JP"> <Head>  <title></title> </Head> <body> <Main /> <NextScript /> </body> </Html> ) } } export default MyDocument; </code></pre> <h3 id="index.tsx"><a href="#index.tsx">index.tsx</a></h3> <pre><code class="js">export interface Work{ image:{ recommendedImageUrl:string }, twitterUsername:string, annictId:number, watchersCount:number, title:string, seasonYear:string, seasonName:string, wikipediaUrl:string, syobocalTid:string } const Index:NextPage = () => { const [pageNum,setPageNum] = useState(1); const [data,setData] = useState<Work[]>(); const store = useContext(storeContext); useEffect(() => { const f = async()=>{ let resData; if(store.query.length === 0){ resData = await fetchSeasonWorks(store.seasonName); }else{ resData = await fetchByTitle(store.query); } setData(resData); console.log(resData); } f(); },[store.seasonName,store.query]); const myRef = useRef<HTMLInputElement>(null); .... </code></pre> <h2 id="hooks"><a href="#hooks">hooks</a></h2> <p>hooksを格納する....はずなのですが、ビューとロジック分離するの失敗してるので<br /> またどこかで使うでしょう(笑)</p> <h2 id="context"><a href="#context">context</a></h2> <p>今回はreduxの代わりにcontextで全体の状態を管理します。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/ragnar1904/items/0a4338523863922952bb">【React + Typescript】useContext の値を子コンポーネントから更新</a></p> <pre><code class="js">type StoreContext = { seasonName: string; query:string; setSeasonName: (seasonName: string) => void; setQuery: (query: string) => void; }; const defaultContext: StoreContext = { seasonName: CURRENT_SEASON, query:'', setSeasonName: () => {}, setQuery: () => {}, }; export const storeContext = createContext<StoreContext>(defaultContext); export const useSeasonName = (): StoreContext => { // state名はThemeContext typeのプロパティに合わせる。 const [seasonName, setSeason] = useState(CURRENT_SEASON); const [query, setQ] = useState(''); // 関数名はThemeContext typeのプロパティに合わせる。 const setSeasonName = useCallback((current: string): void => { setSeason(current); }, []); const setQuery = useCallback((current: string): void => { setQ(current); }, []); return { seasonName, query, setSeasonName, setQuery }; }; </code></pre> <p>個々のコンポーネントでも状態を持っています。</p> <h2 id="gtag"><a href="#gtag">gtag</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/b95oss/items/0402d2a0fa0edeecb67c">Next.jsでGoogle Analyticsを適用する</a></p> <h1 id="lintに苦戦..."><a href="#lint%E3%81%AB%E8%8B%A6%E6%88%A6...">lintに苦戦...</a></h1> <p>がっつりtypescriptを書いていたわけではなかったので、<br /> nextjsはなかなかビルドを通してくれませんでした。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/zeit/next.js/issues/10284">ParsedUrlQuery typings should allow undefined values</a></p> <pre><code class="js">function isString(value: unknown): value is string { return typeof value === "string" } const id = isString(router.query.id) ? router.query.id : "" useEffect(()=>{ const f = async () => { if(router !== undefined && router.query !== undefined){ const resData = await fetchByWorkId(id); setData(resData); } } f(); },[router.query]); </code></pre> <h1 id="netlifyにデプロイ"><a href="#netlify%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">netlifyにデプロイ</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/mottox2/items/29fbb55129f7c41f1ae6">Next.jsのDynamic Routing + Static HTML exportを組み合わせて使う</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/routing/redirects/#syntax-for-the-netlify-configuration-file">Redirects and rewrites</a></li> </ul> <p><code>netlify.toml</code>に必要なルーティングを記述します。</p> <pre><code>[build] base = "/" # next exportの出力先 publish = "out/" # next buildからのnext exportでファイル生成 command = "yarn run build && yarn run export" [context.production] command = "NODE_ENV=production yarn run build && yarn run export" [[redirects]] from = "/" to = "/index.html" status = 200 [[redirects]] from = "/works/*" to = "/works/[id].html" status = 200 </code></pre> ckoshien tag:crieit.net,2005:PublicArticle/15888 2020-05-08T19:25:38+09:00 2020-05-08T19:25:38+09:00 https://crieit.net/posts/b3c547c34df6393a1f86f8072aaf510a アニメのレコメンドサービスを作りました。 <p><a href="https://crieit.now.sh/upload_images/b0f24117575660588f657c26c91d370e5eb525f79a7b2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b0f24117575660588f657c26c91d370e5eb525f79a7b2.png?mw=700" alt="" /></a></p> <h1 id="サービスURL"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9URL">サービスURL</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://annict-suggest.netlify.app/">https://annict-suggest.netlify.app/</a></p> <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55eb5336deb10d.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55eb5336deb10d.jpg?mw=700" alt="" /></a></p> <h1 id="アニメの類似性をどう計算するか"><a href="#%E3%82%A2%E3%83%8B%E3%83%A1%E3%81%AE%E9%A1%9E%E4%BC%BC%E6%80%A7%E3%82%92%E3%81%A9%E3%81%86%E8%A8%88%E7%AE%97%E3%81%99%E3%82%8B%E3%81%8B">アニメの類似性をどう計算するか</a></h1> <h2 id="コサイン類似度"><a href="#%E3%82%B3%E3%82%B5%E3%82%A4%E3%83%B3%E9%A1%9E%E4%BC%BC%E5%BA%A6">コサイン類似度</a></h2> <p>人工知能を使わずにアニメのレコメンドサービスを作ろうと思ったのがきっかけです。<br /> <a href="https://crieit.net/posts/5308d8a3ed140ecc15e1310dad28e9e9">ユークリッド距離は触ったことがある</a>ので、他の指標としてコサイン類似度が面白そうだと思いました。<br /> ユークリッド距離は2点間の距離、コサイン類似度は2点のベクトル同士の角度です。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.albert2005.co.jp/knowledge/data_mining/cluster/cluster_summary">クラスター分析の手法①(概要)</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tetsutaroendo/items/61942d25ae2a017831f2">コサイン類似度を利用し、集団の類似性を測ってみる</a></li> </ul> <h3 id="使った指標"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8C%87%E6%A8%99">使った指標</a></h3> <p>約3300の作品に対して「見た」「見てない」のベクトルを作ってコサイン類似度を算出しようとしました。<br /> 「あにこれ」のように成分分析されているタグの類似度を計算するのもありだと思いました。</p> <h3 id="挫折"><a href="#%E6%8C%AB%E6%8A%98">挫折</a></h3> <p>導入は比較的楽なように思えたのですが、計算量が尋常ではありませんでした。事前にフィルタリングを何もかけていなかったため、3300レコードx3300レコードの計算をしようとしていて、あまりに時間がかかるので諦めました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/harperfu6/items/3238d8f78c8a8d8cf114">アイテムの類似性について考察してみる</a></li> </ul> <h3 id="結局SQL"><a href="#%E7%B5%90%E5%B1%80SQL">結局SQL</a></h3> <p>例:ID4342の作品を見たユーザを抽出して、<br /> それらのユーザが他に見た作品のうち共通している人数が多い順に30件を取得する。</p> <pre><code class="sql">select sum(st2.watch_status) as count, st2.work_id, w.title from status st2 -- ID:4342の作品を見たユーザを取得 inner join ( select st.user_id from status st where st.work_id = 4342 )st3 on st2.user_id = st3.user_id inner join works w on w.annict_id = st2.work_id -- 作品自身を除く where st2.work_id != 4342 group by st2.work_id order by count desc limit 30 </code></pre> <h1 id="今回使った技術"><a href="#%E4%BB%8A%E5%9B%9E%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">今回使った技術</a></h1> <ul> <li>GraphQL(Annict API)</li> <li>ReactJS</li> <li>NodeJS(TypeScript)</li> <li>twitterAPI</li> <li>netlify</li> </ul> <h2 id="データの棲み分け"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%A3%B2%E3%81%BF%E5%88%86%E3%81%91">データの棲み分け</a></h2> <p>最新のデータが欲しい場合はAnnictAPI(GraphQL)、分析データが欲しい場合はDBから読み込み、というようにデータの棲み分けを行っています。</p> <h2 id="GraphQLでエイリアスを使う"><a href="#GraphQL%E3%81%A7%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86">GraphQLでエイリアスを使う</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://developers.annict.jp/graphql-api/reference/">Annict API</a><br /> ユーザが見たアニメと見ているアニメ両方が欲しい場合、<br /> エイリアスを使うと複数条件が記述できる。</p> <pre><code class="javascript">query { user(username:"${username}"){ annictId, works(state:WATCHED){ nodes{ annictId title } } ing:works(state:WATCHING){ nodes{ annictId title } } } } </code></pre> <h2 id="CSP(コンテンツセキュリティポリシー)"><a href="#CSP%28%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%29">CSP(コンテンツセキュリティポリシー)</a></h2> <p>アニメのOGPがない場合は公式twitterアカウントの画像を使用しているが、CSPなどで同じサイトでないコンテンツは表示できなくなったので、<br /> URLに「twitter」が含まれる場合はサーバにプロキシさせて画像を読み込むようにした。</p> <h1 id="ご意見"><a href="#%E3%81%94%E6%84%8F%E8%A6%8B">ご意見</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://ikens.net/ckoshien_tech/annict-suggest?v=1">こちら</a>から使ってみた感想・ご意見をお寄せください。</p> ckoshien tag:crieit.net,2005:PublicArticle/15875 2020-04-27T21:53:23+09:00 2020-04-27T21:53:23+09:00 https://crieit.net/posts/slack-ver3 slack流量計ver3をリリースしました。 <h1 id="slack流量計とは"><a href="#slack%E6%B5%81%E9%87%8F%E8%A8%88%E3%81%A8%E3%81%AF">slack流量計とは</a></h1> <p>サービス運営者コミュニティ「<a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild?page=1">運営者ギルド</a>」のslackの統計情報を可視化するアプリケーションです。</p> <ul> <li><a href="https://crieit.net/posts/slack">ver2リリース記事</a></li> </ul> <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625ea6c755c0971.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625ea6c755c0971.jpg?mw=700" alt="" /></a></p> <h2 id="ver3で何が変わったの?"><a href="#ver3%E3%81%A7%E4%BD%95%E3%81%8C%E5%A4%89%E3%82%8F%E3%81%A3%E3%81%9F%E3%81%AE%EF%BC%9F">ver3で何が変わったの?</a></h2> <h3 id="VPSに移設"><a href="#VPS%E3%81%AB%E7%A7%BB%E8%A8%AD">VPSに移設</a></h3> <p>これまでslackに統計情報を集計するbotアプリケーションとして運用していましたが、サーバをVPSに移設しました。</p> <h3 id="WEBアプリケーション化"><a href="#WEB%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%8C%96">WEBアプリケーション化</a></h3> <p>貴重なslackのリソースを消費せずにWEBで閲覧できるようになりました。(要slackサインイン)</p> <h3 id="puppeteerでのスクリーンショットは廃止"><a href="#puppeteer%E3%81%A7%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%81%AF%E5%BB%83%E6%AD%A2">puppeteerでのスクリーンショットは廃止</a></h3> <h2 id="使っている技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E6%8A%80%E8%A1%93">使っている技術</a></h2> <ul> <li>ReactJS</li> <li>NodeJS/Express/TypeScript</li> <li>passport</li> <li>node-cron</li> <li>chart.js</li> <li>slack/client</li> </ul> <h1 id="技術的な話"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E8%A9%B1">技術的な話</a></h1> <h2 id="react-bootstrap-table2をbootstrap CSSを適用せずに作る"><a href="#react-bootstrap-table2%E3%82%92bootstrap+CSS%E3%82%92%E9%81%A9%E7%94%A8%E3%81%9B%E3%81%9A%E3%81%AB%E4%BD%9C%E3%82%8B">react-bootstrap-table2をbootstrap CSSを適用せずに作る</a></h2> <p>分析機能を作ることが多いので、表を多用するのですが、<a target="_blank" rel="nofollow noopener" href="https://react-bootstrap-table.github.io/react-bootstrap-table2/docs/getting-started.html">react-bootstrap-table2</a>というコンポーネントを結構使っています。</p> <p>これまではbootstrap CSSを適用した後にこれでもかというぐらいCSSを上書きしていたのですが、今回は自力でキャレットを実装できました。<br /> ページネーションも<code>list-style-type: none;</code>に気づいてサクサク実装。<br /> CSSいじるの楽しい!</p> <pre><code class="scss">th{ font-size: 12px; .dropdown>.caret{ &::after{ font-family: "Font Awesome 5 Free"; content: "\f0d7"; } } .dropup>.caret{ &::after{ font-family: "Font Awesome 5 Free"; content: "\f0d8"; } } .caret{ &::after{ font-family: "Font Awesome 5 Free"; content: "\f0d7"; } } } ul.pagination{ display: flex; list-style-type: none; text-align: center; li.page-item{ >a.page-link{ text-decoration: none; background-color: #555555; padding: 3px; margin-right: 3px; color: white; } &.active > a.page-link{ text-decoration: none; background-color: #bfbfbf; padding: 3px; margin-right: 3px; color: #555555; } } } </code></pre> <h2 id="passportを使ったslackサインインの実装"><a href="#passport%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Fslack%E3%82%B5%E3%82%A4%E3%83%B3%E3%82%A4%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A3%85">passportを使ったslackサインインの実装</a></h2> <p>これは前回<a href="https://crieit.net/posts/React-NodeJS-Passport-twitter">twitterサインインの実装をした</a>のが役に立ちました。</p> <p>以下ソース抜粋</p> <pre><code class="javascript">import passportConfig from './passportConfig'; import * as session from 'express-session'; const MySQLStore = require('express-mysql-session')(session); passportConfig(); const passport = require('passport'); app.use(passport.initialize()); app.use(passport.session()); app.use( session({ secret: '******', resave: false, store: new MySQLStore(dbConfig), saveUninitialized: false, cookie:{ httpOnly: false, secure: false, maxage: 1000 * 60 } }) ); </code></pre> <p>passport-slackはシリアライズ/デシリアライズを使っていないようなので、<br /> 実装個所は変わりますが、認証が成功した際にセッションに情報を格納するといけました。</p> <pre><code class="javascript">(req: Request, res) => { req.session.user = req.account; res.redirect(BACKEND); } </code></pre> ckoshien tag:crieit.net,2005:PublicArticle/15862 2020-04-23T00:32:48+09:00 2020-04-23T00:32:48+09:00 https://crieit.net/posts/b54c153b71a3e506c51b61480e03c38c 自分だけのオリジナル紙面を作れる「スポーツ新聞メーカー」をリリースしました。 <h1 id="サービスURL"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9URL">サービスURL</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://sports-news-maker.netlify.app">https://sports-news-maker.netlify.app</a></li> </ul> <h1 id="何ができるか"><a href="#%E4%BD%95%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%8B">何ができるか</a></h1> <p><a href="https://crieit.now.sh/upload_images/668008c5ca92ae17eb989010b49994385ea05f1a6faea.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/668008c5ca92ae17eb989010b49994385ea05f1a6faea.jpg?mw=700" alt="" /></a></p> <ul> <li>新聞名</li> <li>見出し</li> <li>試合のスコア(3-10イニングまで)</li> <li>画像</li> <li>縦見出し</li> <li>写真のキャプション</li> </ul> <p>を編集してこのようなオリジナル紙面を作れます。</p> <h1 id="特徴"><a href="#%E7%89%B9%E5%BE%B4">特徴</a></h1> <h2 id="入力フォームの隠蔽"><a href="#%E5%85%A5%E5%8A%9B%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E9%9A%A0%E8%94%BD">入力フォームの隠蔽</a></h2> <p><a href="https://crieit.net/posts/1bbf25a7cba4095f0fd1afe817b553dd">パワプロ風画面ジェネレータ</a>で作ったコンポーネントを流用しています。<br /> クリックするまで入力フォームが出ない仕様です。</p> <h2 id="縦書きの見出し"><a href="#%E7%B8%A6%E6%9B%B8%E3%81%8D%E3%81%AE%E8%A6%8B%E5%87%BA%E3%81%97">縦書きの見出し</a></h2> <p>縦書きをCSSでデザインしています。<br /> writing-modeというプロパティだけでは、英文字が横になってしまうので、text-orientationというプロパティで英文字対応を行っています。</p> <pre><code class="javascript">style=<span>{</span><span>{</span> writingMode:'vertical-rl', textOrientation:'upright', fontSize:`calc(6vw - ${store.getState().newsTitle.length/2}px )`, margin:5, width:'1em', whiteSpace:'nowrap', fontWeight:'bold' <span>}</span><span>}</span> </code></pre> <h2 id="レスポンシブ"><a href="#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B7%E3%83%96">レスポンシブ</a></h2> <p>フォントサイズや画像の大きさ、マージンなどを画面の幅によって変わるようCSSで実装しています。</p> <h2 id="可変イニング数"><a href="#%E5%8F%AF%E5%A4%89%E3%82%A4%E3%83%8B%E3%83%B3%E3%82%B0%E6%95%B0">可変イニング数</a></h2> <p>3~10回までイニング数を変えることができるようになっています。</p> <h2 id="セピアフィルター"><a href="#%E3%82%BB%E3%83%94%E3%82%A2%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%BC">セピアフィルター</a></h2> <p>画像に新聞らしくセピアフィルターをかけています。<br /> へぇ、CSSってこんなこともできるんだ!という新しい発見がありますね。</p> <pre><code class="html"><img style=<span>{</span><span>{</span> maxWidth:'85vw', objectFit:'cover', filter:'sepia(30%)', <span>}</span><span>}</span> src={this.state.image_src}/> </code></pre> <h1 id="振り返り"><a href="#%E6%8C%AF%E3%82%8A%E8%BF%94%E3%82%8A">振り返り</a></h1> <p>今回はほぼ一日で作ったので、またフロントで完結しています。<br /> 時間があればOGPでシェア機能つけたら面白いかもしれません。<br /> あとは試合スコアのチーム名を長くすると表示が乱れるのは課題です。<br /> 幅を固定してフォントサイズを文字長で反比例させればいいですかね。</p> ckoshien tag:crieit.net,2005:PublicArticle/15828 2020-04-14T20:37:13+09:00 2020-04-14T20:37:13+09:00 https://crieit.net/posts/React-NodeJS-Passport-twitter React/NodeJS/Passportでtwitterログインを実装してみた <p>こちらの記事をベースにしてReactJS/NodeJSのシステムにtwitterログインを組み込んでみた。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/itagakishintaro/items/e5a0481b51e6a17b304c">https://qiita.com/itagakishintaro/items/e5a0481b51e6a17b304c</a></p> <p>主に異なるのは型指定が緩めな<code>なんちゃって</code>typescriptを使っているところか。</p> <h1 id="実装したもの"><a href="#%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F%E3%82%82%E3%81%AE">実装したもの</a></h1> <h2 id="蓋々交換機能"><a href="#%E8%93%8B%E3%80%85%E4%BA%A4%E6%8F%9B%E6%A9%9F%E8%83%BD">蓋々交換機能</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://cap-baseball.com/cap_exchange">https://cap-baseball.com/cap_exchange</a></p> <h1 id="技術的なこと"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E3%81%93%E3%81%A8">技術的なこと</a></h1> <h2 id="passport-config.ts"><a href="#passport-config.ts">passport-config.ts</a></h2> <pre><code class="javascript">export default function passportConfig() { var TWITTER_CONSUMER_KEY = "*****"; var TWITTER_CONSUMER_SECRET = "*******"; var passport = require("passport"), TwitterStrategy = require("passport-twitter").Strategy; // Sessionの設定 passport.serializeUser(function (user, done) { done(null, user); }); passport.deserializeUser(function (obj, done) { done(null, obj); }); passport.use( new TwitterStrategy( { consumerKey: TWITTER_CONSUMER_KEY, consumerSecret: TWITTER_CONSUMER_SECRET, callbackURL: "https://********/auth/twitter/callback", }, function (token, tokenSecret, profile, done) { passport.session.user = profile; // tokenとtoken_secretをセット profile.twitter_token = token; profile.twitter_token_secret = tokenSecret; process.nextTick(function () { return done(null, profile); }); } ) ); } </code></pre> <h2 id="auth.ts"><a href="#auth.ts">auth.ts</a></h2> <p>NodeJSでtwitter認証からのコールバックなどを担当するコントローラ。</p> <pre><code class="javascript">import * as express from "express"; import * as session from 'express-session'; import { Request } from "./interface/express.Request"; const passport = require("passport"); export class Auth{ public router: express.Router; constructor() { this.router = express.Router(); this.router.get("/twitter", passport.authenticate('twitter')); this.router.get("/twitter/success", this.success); this.router.get("/twitter/callback", passport.authenticate('twitter', { successRedirect: 'https://******/api/v2/auth/twitter/success', failureRedirect: 'https://******/' }) )} private success(req:Request,res:express.Response):void{ if(req.session.passport !== undefined){ res.json(req.session.passport.user.username); }else{ res.sendStatus(401); } } } </code></pre> <h2 id="express.Requestインターフェースの拡張"><a href="#express.Request%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%81%AE%E6%8B%A1%E5%BC%B5">express.Requestインターフェースの拡張</a></h2> <pre><code class="javascript">import * as Express from 'express'; export interface Request extends Express.Request { session:any; } </code></pre> <h2 id="app.ts"><a href="#app.ts">app.ts</a></h2> <pre><code class="javascript">import { Auth } from './auth'; import * as session from 'express-session'; import passportConfig from './passport-config'; passportConfig(); const passport = require("passport"); app.use(passport.initialize()); app.use(passport.session()); app.use( session({ secret: '********', resave: false, saveUninitialized: false, cookie:{ httpOnly: true, secure: true, maxage: 1000 * 60 * 30 } }) ); </code></pre> <h2 id="ReactでNodeJS/Passportから認証情報を受け取る"><a href="#React%E3%81%A7NodeJS%2FPassport%E3%81%8B%E3%82%89%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8B">ReactでNodeJS/Passportから認証情報を受け取る</a></h2> <p>credentialsオプションが必要。</p> <pre><code class="javascript"> const response = await fetch('/auth/twitter/success', { method:'GET', credentials: "include", headers: { Accept: "application/json", "Content-Type": "application/json", "Access-Control-Allow-Credentials": true } }); </code></pre> <h2 id="認証が終わったタイミングで認証情報を受け取る"><a href="#%E8%AA%8D%E8%A8%BC%E3%81%8C%E7%B5%82%E3%82%8F%E3%81%A3%E3%81%9F%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%A7%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8B">認証が終わったタイミングで認証情報を受け取る</a></h2> <p>認証のためのウインドウを開き、<br /> そのウインドウが閉じられたタイミングで親の画面をリロードする。<br /> リロードの際に認証情報を受け取っている。</p> <pre><code class="javascript">onClick={()=>{ const authWindow = window.open('/auth/twitter','newTab'); var timer = setInterval(function() { if(authWindow.closed) { clearInterval(timer); window.location.reload(); } }, 1000); <span>}</span><span>}</span> </code></pre> ckoshien tag:crieit.net,2005:PublicArticle/15766 2020-03-14T22:03:11+09:00 2020-03-14T22:04:12+09:00 https://crieit.net/posts/1bbf25a7cba4095f0fd1afe817b553dd (改良版)パワプロ風画面ジェネレータを作ってみた <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg?mw=700" alt="" /></a></p> <h1 id="作ったサービス"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">作ったサービス</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://pawapro-gen.netlify.com/">https://pawapro-gen.netlify.com/</a></p> <h1 id="一旦書いたボード"><a href="#%E4%B8%80%E6%97%A6%E6%9B%B8%E3%81%84%E3%81%9F%E3%83%9C%E3%83%BC%E3%83%89">一旦書いたボード</a></h1> <p><a href="https://crieit.net/boards/web1week-202003/6fbd2929361eb9caa53fc33d51d31f36">web1week - パワプロ風画面ジェネレータを作ってみた</a></p> <p>時間的に残りの機能間に合わないかと思っていたので、最低限動く機能で出したのですが、結論から言うと間に合いました。</p> <h1 id="インライン編集機能"><a href="#%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E7%B7%A8%E9%9B%86%E6%A9%9F%E8%83%BD">インライン編集機能</a></h1> <p>当初はフォームとプレビュー機能を分けて作っていたのですが、<br /> ほとんど全てインラインで編集できるように変更しました。</p> <h1 id="twitterシェア機能"><a href="#twitter%E3%82%B7%E3%82%A7%E3%82%A2%E6%A9%9F%E8%83%BD">twitterシェア機能</a></h1> <h2 id="OGPサーバの構築"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89">OGPサーバの構築</a></h2> <p>以前<a href="https://crieit.net/posts/slack">slack流量計</a>を作った際に、web表彰のスクリーンショットを撮っていたので、OGP作成にはそれほど抵抗はありませんでした。</p> <p>あとはだらさんの作成されてる<a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/puppeteer-ogp">OGPサーバ</a>を参考にさせていただきました。</p> <h3 id="OGPサーバの構築に使った技術"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">OGPサーバの構築に使った技術</a></h3> <ul> <li>NodeJS</li> <li>Express</li> <li>TypeScript</li> <li>puppeteer</li> <li>VPS</li> <li>docker-compose</li> </ul> <h2 id="ロジック"><a href="#%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF">ロジック</a></h2> <ul> <li>ジェネレータからDBにデータを登録する</li> <li>登録されたデータをもとにpuppeteerでスクリーンショットを撮る</li> <li>DBに画像を格納する</li> <li>OGPのサーバ問い合わせにはDBに保存してある画像を使用する <ul> <li>OGPサーバに直接アクセスさせると負荷が集中してサーバが落ちるため</li> </ul></li> </ul> <h3 id="DB登録処理は既存のAPサーバに追加"><a href="#DB%E7%99%BB%E9%8C%B2%E5%87%A6%E7%90%86%E3%81%AF%E6%97%A2%E5%AD%98%E3%81%AEAP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AB%E8%BF%BD%E5%8A%A0">DB登録処理は既存のAPサーバに追加</a></h3> <h3 id="OGPサーバは別コンテナ"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AF%E5%88%A5%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A">OGPサーバは別コンテナ</a></h3> <h2 id="twitter投稿"><a href="#twitter%E6%8A%95%E7%A8%BF">twitter投稿</a></h2> <p>アプリ起動時<code>componentDidMount</code>のタイミングでUUIDを生成するようにしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/react-share">react-share</a>というライブラリを使い、<br /> <code>beforeOnClick</code>メソッドでDBへの登録を行っています。<br /> ローディング画面も作りました(同時にバグ生成)。</p> <pre><code class="html"><TwitterShareButton url={'https://pawapro-gen.netlify.com/view/'+store.getState().uuid} hashtags={['パワプロ風画面ジェネレータで作ってみた']} beforeOnClick={async()=>{ store.dispatch(switchLoading(true)); let isSuccess = await postData(); store.dispatch(switchLoading(false)); if(!isSuccess){ alert('データの保存に失敗しました。時間を空けるか、リロードして再度作成してください。') return new Error(); }else{ return Promise.resolve(); } <span>}</span><span>}</span> > 保存してシェアする! </TwitterShareButton> </code></pre> <h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1> <h2 id="画像のキャッシュ"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5">画像のキャッシュ</a></h2> <p>ヘッダでキャッシュコントロールしたことがなかったので若干はまりました。</p> <h2 id="OGPのトリミング問題"><a href="#OGP%E3%81%AE%E3%83%88%E3%83%AA%E3%83%9F%E3%83%B3%E3%82%B0%E5%95%8F%E9%A1%8C">OGPのトリミング問題</a></h2> <p>twitterやFacebookでOGPの上下左右がトリミングされる問題があり、<br /> Jimpなどで余白の拡張をしようと思ったのですが、時間がなかったこともあり、<br /> React側でマージンをとってスクリーンショットを撮る際に余白ができるように修正しました。</p> <h1 id="できなかったこと"><a href="#%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">できなかったこと</a></h1> <h2 id="チーム・選手画像のアップロード"><a href="#%E3%83%81%E3%83%BC%E3%83%A0%E3%83%BB%E9%81%B8%E6%89%8B%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89">チーム・選手画像のアップロード</a></h2> <p>OGPの生成で手一杯だったので、base64化してDBに格納するところまで考えられませんでした。後々追加する予定。</p> ckoshien tag:crieit.net,2005:PublicArticle/15677 2020-01-13T14:41:18+09:00 2020-01-13T14:44:52+09:00 https://crieit.net/posts/1626f2bc2824aa0dbd4d3bee680dc2fe 共通の設定をリポジトリ化する <h1 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h1> <p>現在、私が運営しているサービスのうち、</p> <p>野球スコア掲載プラットフォーム<br /> <a target="_blank" rel="nofollow noopener" href="https://jcbl-score.com">みんなのSCORE</a><br /> キャップ野球総合サイト<br /> <a target="_blank" rel="nofollow noopener" href="https://cap-baseball.com">キャップ野球情報局</a></p> <p>この2サービス、共通のアプリケーションサーバを持っていてUIが異なるだけなのです。</p> <p>画像や設定など、2つのサイト(異なるリポジトリ)間で同じものを共有したい場合、<br /> 別のリポジトリから<code>npm install</code>するのがよいようです。</p> <h1 id="実際に作ってみる"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B">実際に作ってみる</a></h1> <p>今回は試合のIDとyoutubeやtwitterのツイートを紐づけするconfigファイルを<br /> 共通リポジトリに置きます。</p> <h2 id="今回作ったリポジトリのindex.js"><a href="#%E4%BB%8A%E5%9B%9E%E4%BD%9C%E3%81%A3%E3%81%9F%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AEindex.js">今回作ったリポジトリのindex.js</a></h2> <pre><code class="javascript">export { twMovieConfig } from './config/twMovieConfig'; export { youtubeConfig } from './config/youtubeConfig'; </code></pre> <p>共有したいファイルをexportする。</p> <h2 id="package.json"><a href="#package.json">package.json</a></h2> <pre><code class="json">{ "name": "common", "version": "0.0.202001131200", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "description": "" } </code></pre> <h2 id="インポートする側のpackage.json"><a href="#%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%E5%81%B4%E3%81%AEpackage.json">インポートする側のpackage.json</a></h2> <pre><code class="js"> { "dependencies": { ........ "common": "https://github.com/ckoshien/common.git" } } </code></pre> <h1 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h1> <ul> <li>変更があった場合は毎回npmインストール</li> <li>package-lock.jsonを削除しないとインストールできない</li> <li>package.jsonのバージョンを上げないと変更があったと認識されない</li> </ul> ckoshien tag:crieit.net,2005:PublicArticle/15660 2019-12-31T13:33:23+09:00 2019-12-31T13:33:23+09:00 https://crieit.net/posts/2019-UI 2019年はUI力を上げた年だった <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dd018433068a.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dd018433068a.jpg?mw=700" alt="" /></a><br /> 2019年、色々なものを作ったので少しずつ振り返ろうと思います。</p> <h1 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h1> <p>元々、業務系のサーバサイドのプログラムを得意としていたのもあって、WEBサービスのUIは全く無関心というかCSSの中身もよくわからない状態でした。</p> <h1 id="作ったものを振り返る"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%E3%82%92%E6%8C%AF%E3%82%8A%E8%BF%94%E3%82%8B">作ったものを振り返る</a></h1> <p>画像多めですが、駆け足で振り返ります。<br /> UIに目覚めたのは8月のぐらいでしょうか。<br /> 10月後半からは怒涛の毎週リリース(毎週末何か作っては発表)をしていました。</p> <h2 id="1/4 slack流量計"><a href="#1%2F4+slack%E6%B5%81%E9%87%8F%E8%A8%88">1/4 slack流量計</a></h2> <p><a href="https://crieit.now.sh/upload_images/7a7957261387b24d52e646e155b1c2b15c30957b80170.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a7957261387b24d52e646e155b1c2b15c30957b80170.png?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/slack">slack流量計の新バージョンをリリースしました</a><br /> 運営者ギルドで使っているslackの投稿数を可視化するツールです。<br /> あぁ、これまだ2019年でしたか....。</p> <h2 id="2月-6月"><a href="#2%E6%9C%88-6%E6%9C%88">2月-6月</a></h2> <h3 id="2月"><a href="#2%E6%9C%88">2月</a></h3> <p><a href="https://crieit.now.sh/upload_images/b08b548caec5795ad40e35af9c85f4c95c77f39d0e632.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b08b548caec5795ad40e35af9c85f4c95c77f39d0e632.png?mw=700" alt="" /></a><br /> 2月末。試行錯誤していますが、振り返ると首を傾げたくなるデザイン...。</p> <h3 id="4月"><a href="#4%E6%9C%88">4月</a></h3> <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625cc659b1e56d5.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625cc659b1e56d5.jpg?mw=700" alt="" /></a><br /> 今の野球成績管理システムはこのあたりでfixした模様。</p> <h3 id="5-6月"><a href="#5-6%E6%9C%88">5-6月</a></h3> <p>このあたりから野球スコア管理システムのver3を製造開始。</p> <h2 id="8/18 タイピングスコア管理システム"><a href="#8%2F18+%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%82%B9%E3%82%B3%E3%82%A2%E7%AE%A1%E7%90%86%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0">8/18 タイピングスコア管理システム</a></h2> <p><a href="https://crieit.now.sh/upload_images/c6a4b4840dc7b0443a48a1205e7fb4455d593c36cd7a1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c6a4b4840dc7b0443a48a1205e7fb4455d593c36cd7a1.png?mw=700" alt="" /></a></p> <p><a href="https://crieit.net/posts/cb42a72a9e70af9ea472af216219e0e8">タイピングスコア管理システムを作った</a></p> <p>社内向けのタイピング練習システム。このあたりから若干UI凝り始めた。</p> <h2 id="8/21 キャップ全国大会非公式サイト"><a href="#8%2F21+%E3%82%AD%E3%83%A3%E3%83%83%E3%83%97%E5%85%A8%E5%9B%BD%E5%A4%A7%E4%BC%9A%E9%9D%9E%E5%85%AC%E5%BC%8F%E3%82%B5%E3%82%A4%E3%83%88">8/21 キャップ全国大会非公式サイト</a></h2> <p><a href="https://crieit.now.sh/upload_images/b0f24117575660588f657c26c91d370e5d5d2ca647db2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b0f24117575660588f657c26c91d370e5d5d2ca647db2.png?mw=700" alt="" /></a><br /> <a href="https://crieit.net/boards/baseball-score-management/f51b909e24c84342ac946117b65d092e">キャップ野球全国大会の非公式特設サイトを作った</a></p> <p>このサイトが事実上の転機になりました。<br /> 自分でUIを作るのに試行錯誤してCSS書きまくって...桜吹雪まで飛ばしてます。このあたりからUIの大切さとかUIへのこだわりとかが生まれた気がします。</p> <h2 id="9/9 CSSでtrans-am"><a href="#9%2F9+CSS%E3%81%A7trans-am">9/9 CSSでtrans-am</a></h2> <p><a href="https://crieit.now.sh/upload_images/b08b548caec5795ad40e35af9c85f4c95d764f1a11e8a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b08b548caec5795ad40e35af9c85f4c95d764f1a11e8a.png?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/CSS">CSSアニメーションでトランザム!</a><br /> CSSアニメーションの作り方が何となくわかった回。</p> <h2 id="10/6 野球スコア管理システムver3"><a href="#10%2F6+%E9%87%8E%E7%90%83%E3%82%B9%E3%82%B3%E3%82%A2%E7%AE%A1%E7%90%86%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0ver3">10/6 野球スコア管理システムver3</a></h2> <p><a href="https://crieit.now.sh/upload_images/b08b548caec5795ad40e35af9c85f4c95dac57eabfff8.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b08b548caec5795ad40e35af9c85f4c95dac57eabfff8.png?mw=700" alt="" /></a><br /> <a href="https://crieit.net/boards/baseball-score-management/10-6"></a></p> <h2 id="10/21 ドラフトなう!"><a href="#10%2F21+%E3%83%89%E3%83%A9%E3%83%95%E3%83%88%E3%81%AA%E3%81%86%EF%BC%81">10/21 ドラフトなう!</a></h2> <p><a href="https://crieit.now.sh/upload_images/c92cd6ef54296b6dfb31726f0b708d715dacf6b4b06a6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c92cd6ef54296b6dfb31726f0b708d715dacf6b4b06a6.png?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/4b0c9a1ddc05a0cc0ff61db75530b9b0">ドラフト風画像作成サービス「ドラフトなう!」を作りました</a></p> <p>ドラフト会議に間に合わなかったやつですね!</p> <h2 id="10/27 アニメランキング作成サービス"><a href="#10%2F27+%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%A9%E3%83%B3%E3%82%AD%E3%83%B3%E3%82%B0%E4%BD%9C%E6%88%90%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">10/27 アニメランキング作成サービス</a></h2> <p><a href="https://crieit.now.sh/upload_images/f2fa429ca90221dc87d712c2a35832965db586d228738.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f2fa429ca90221dc87d712c2a35832965db586d228738.jpg?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/AnnictAccess3">アニメランキング作成サービスをリニューアルしました</a></p> <h2 id="11/17 音楽ランキングメーカー"><a href="#11%2F17+%E9%9F%B3%E6%A5%BD%E3%83%A9%E3%83%B3%E3%82%AD%E3%83%B3%E3%82%B0%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC">11/17 音楽ランキングメーカー</a></h2> <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dd018433068a.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dd018433068a.jpg?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/509e31a0bbc4ffe7a1003278816c9e5d">音楽ランキングメーカーをリリースしました</a></p> <p>Reactのドラッグアンドドロップが気に入ったのもあってUIを流用して別カテゴリでサービスを作ってみた。</p> <h2 id="12/1 優勝ラインシミュレータ"><a href="#12%2F1+%E5%84%AA%E5%8B%9D%E3%83%A9%E3%82%A4%E3%83%B3%E3%82%B7%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF">12/1 優勝ラインシミュレータ</a></h2> <p><a href="https://crieit.now.sh/upload_images/814e7c46897c4976a9690900999ccc1a5de3a91f485a7.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/814e7c46897c4976a9690900999ccc1a5de3a91f485a7.jpg?mw=700" alt="" /></a><br /> <a href="https://crieit.net/boards/baseball-score-management/d3a03b2eecede278f5eb211aaff9a6a2">優勝ラインシミュレーター作りました</a><br /> これはクソ(UI)アプリ。ちょっとしたいきさつがあってクソアプリアドベントカレンダーに載りました。</p> <h2 id="12/2 ボウリング幹事アプリ"><a href="#12%2F2+%E3%83%9C%E3%82%A6%E3%83%AA%E3%83%B3%E3%82%B0%E5%B9%B9%E4%BA%8B%E3%82%A2%E3%83%97%E3%83%AA">12/2 ボウリング幹事アプリ</a></h2> <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dda909adf110.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dda909adf110.jpg?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/d2e5218181c66086dd0e423f1bc427a9">ボウリング幹事アプリ</a><br /> MQTTを使った表示画面同期を実装しました。</p> <h2 id="12/8 イベントカレンダー「ふたびより」"><a href="#12%2F8+%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E3%80%8C%E3%81%B5%E3%81%9F%E3%81%B3%E3%82%88%E3%82%8A%E3%80%8D">12/8 イベントカレンダー「ふたびより」</a></h2> <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15debc00300520.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15debc00300520.jpg?mw=700" alt="" /></a><br /> <a href="https://crieit.net/posts/582e915854052994bfba960a9f0f66a4">イベントカレンダー「ふたびより」</a></p> <h2 id="12/11 キャップ野球チームページ作成(wix)"><a href="#12%2F11+%E3%82%AD%E3%83%A3%E3%83%83%E3%83%97%E9%87%8E%E7%90%83%E3%83%81%E3%83%BC%E3%83%A0%E3%83%9A%E3%83%BC%E3%82%B8%E4%BD%9C%E6%88%90%28wix%29">12/11 キャップ野球チームページ作成(wix)</a></h2> <p><img src="https://static.wixstatic.com/media/a067ea_232509a818894a5bb8634b0cf45df201~mv2_d_2024_1262_s_2.jpg/v1/fit/w_2500,h_1330,al_c/a067ea_232509a818894a5bb8634b0cf45df201~mv2_d_2024_1262_s_2.jpg" alt="" /><br /> <a target="_blank" rel="nofollow noopener" href="https://ckoshien1.wixsite.com/ts-cappers">東京世田谷キャッパーズ</a></p> <h2 id="12/14 キャップ野球情報局"><a href="#12%2F14+%E3%82%AD%E3%83%A3%E3%83%83%E3%83%97%E9%87%8E%E7%90%83%E6%83%85%E5%A0%B1%E5%B1%80">12/14 キャップ野球情報局</a></h2> <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15df4fa5e69108.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15df4fa5e69108.jpg?mw=700" alt="" /></a><br /> <a href="https://crieit.net/boards/baseball-score-management/CMS">CMS風サイトを作りました</a></p> ckoshien tag:crieit.net,2005:PublicArticle/15634 2019-12-22T23:48:32+09:00 2019-12-22T23:48:32+09:00 https://crieit.net/posts/react-big-calendar react-big-calendarの攻略 <h1 id="イベントカレンダー機能"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E6%A9%9F%E8%83%BD">イベントカレンダー機能</a></h1> <p>Reactで作った<a href="https://crieit.net/boards/baseball-score-management/CMS">CMS</a>にカレンダー機能を実装しました。<br /> <a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55dff7d45dd4f2.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55dff7d45dd4f2.jpg?mw=700" alt="" /></a></p> <h2 id="動作イメージ"><a href="#%E5%8B%95%E4%BD%9C%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">動作イメージ</a></h2> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">カレンダー機能をリリースしました! <a target="_blank" rel="nofollow noopener" href="https://t.co/uY5j3YkkCV">pic.twitter.com/uY5j3YkkCV</a></p>— キャップ野球情報局 (@cap_bb_info) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/cap_bb_info/status/1208555031894474758?ref_src=twsrc%5Etfw">December 22, 2019</a></blockquote> <h2 id="使ったライブラリ"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">使ったライブラリ</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/intljusticemission/react-big-calendar">react-big-calendar</a></li> <li><a target="_blank" rel="nofollow noopener" href="http://intljusticemission.github.io/react-big-calendar/examples/index.html">ライブラリのデモ</a></li> </ul> <h2 id="アジェンダテーブル"><a href="#%E3%82%A2%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%80%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB">アジェンダテーブル</a></h2> <p>theaderとtbodyが連動していないため、OSSに初めて<a target="_blank" rel="nofollow noopener" href="https://github.com/intljusticemission/react-big-calendar/pull/1557">プルリク</a>を出してみました。<br /> とりあえず応急措置としてスタイルで固定で横幅を指定します。</p> <pre><code class="css">th.rbc-header:nth-child(1){ min-width: 77px; } th.rbc-header:nth-child(2){ min-width: 121px; } th.rbc-header:nth-child(3){ width: 100%; } </code></pre> <h2 id="表示の日本語化"><a href="#%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8C%96">表示の日本語化</a></h2> <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55dff7c1f5dfe0.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55dff7c1f5dfe0.jpg?mw=700" alt="" /></a></p> <pre><code class="javascript">import globalize from "globalize"; require("./globalize.ja.js"); const localizer = globalizeLocalizer(globalize); </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://searchcode.com/codesearch/view/15503072/">globalize.culture.ja.js</a></p> <p>localizerはreact-big-calendarにpropsとして渡します。</p> <h2 id="ツールバーの日本語化"><a href="#%E3%83%84%E3%83%BC%E3%83%AB%E3%83%90%E3%83%BC%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8C%96">ツールバーの日本語化</a></h2> <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55dff7bc4c3890.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55dff7bc4c3890.jpg?mw=700" alt="" /></a><br /> こちらはglobalizeに対応していないハードコーディングなので、<br /> CSSで元の文字を0pxにし、要素を増やす形で置き換えます。</p> <pre><code class="css">/*文字置き換え*/ .rbc-btn-group > button{ font-size: 0px; } .rbc-btn-group:nth-child(1) > button:nth-child(1)::before{ font-size: 14px; content: '今日'; } .rbc-btn-group:nth-child(1) > button:nth-child(2)::before{ font-size: 14px; content: '←'; } .rbc-btn-group:nth-child(1) > button:nth-child(3)::before{ font-size: 14px; content: '→'; } .rbc-btn-group:nth-child(3) > button:nth-child(1)::before{ font-size: 14px; content: '月'; } .rbc-btn-group:nth-child(3) > button:nth-child(2)::before{ font-size: 14px; content: '週'; } .rbc-btn-group:nth-child(3) > button:nth-child(3)::before{ font-size: 14px; content: '日'; } .rbc-btn-group:nth-child(3) > button:nth-child(4)::before{ font-size: 14px; content: 'スケジュール'; } </code></pre> <h1 id="props"><a href="#props">props</a></h1> <h2 id="components"><a href="#components">components</a></h2> <p>event(月・週・日で表示されるコンポーネント)とagenda(スケジュールで表示されるイベント)の動作を記述できる。</p> <pre><code class="javascript">Event = ({ event }) => { return ( <span> {event.title} </span>); }; EventAgenda = ({ event }) => { return ( <div onClick={()=>{ this.setState({ isOpen:true, selectEvent:event }) <span>}</span><span>}</span> > {event.type}/{event.title} </div> ); }; components=<span>{</span><span>{</span> event: this.Event, agenda: { event: this.EventAgenda } <span>}</span><span>}</span> </code></pre> <h2 id="onSelectEvent"><a href="#onSelectEvent">onSelectEvent</a></h2> <p>agendaで表示されるコンポーネントが選択されたときに発火するイベント<br /> (ここではポップアップモーダルを開く)</p> <pre><code class="javascript">onSelectEvent={(event,e)=>{ this.setState({ isOpen:true, selectEvent:event }) <span>}</span><span>}</span> </code></pre> <h2 id="eventPropGetter"><a href="#eventPropGetter">eventPropGetter</a></h2> <p>event.typeによってスタイルを変える処理<br /> 参考:<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/34587067/change-color-of-react-big-calendar-events">Change color of react-big-calendar events</a></p> <pre><code class="javascript"><br /> eventPropGetter={(event, start, end, isSelected) => { let bgColor; let fontColor; switch (event.type) { case "練習会": bgColor = "#BF7A87"; fontColor = "aliceblue"; break; default: bgColor = "#386537"; fontColor = "aliceblue"; break; } let newStyle = { backgroundColor: bgColor, color: fontColor }; return { className: "", style: newStyle }; <span>}</span><span>}</span> </code></pre> ckoshien tag:crieit.net,2005:PublicArticle/15587 2019-12-08T00:51:56+09:00 2019-12-12T07:36:34+09:00 https://crieit.net/posts/582e915854052994bfba960a9f0f66a4 キャップ野球向けイベントカレンダー作ってみました。【12/12 update】 <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15debc00300520.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15debc00300520.jpg?mw=700" alt="" /></a></p> <h1 id="キャップ野球向けイベントカレンダー"><a href="#%E3%82%AD%E3%83%A3%E3%83%83%E3%83%97%E9%87%8E%E7%90%83%E5%90%91%E3%81%91%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC">キャップ野球向けイベントカレンダー</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://cap-calendar.netlify.com/">https://cap-calendar.netlify.com/</a></p> <p>最近キャップ野球関係のサービスをいくつか作っているのですが、</p> <ul> <li>キャップ野球・全国大会非公式特設サイト</li> <li>野球リーグスコア管理システム ver3α</li> <li>優勝ラインシミュレーター</li> </ul> <p>まだ普及途上にあるマイナースポーツなので、色々広報手段が足りないようで「<a target="_blank" rel="nofollow noopener" href="https://shogi-sanpo.com/">こんなイベントカレンダー</a>欲しい」という声を聞いたので作ってみました。</p> <h1 id="検討した仕組み"><a href="#%E6%A4%9C%E8%A8%8E%E3%81%97%E3%81%9F%E4%BB%95%E7%B5%84%E3%81%BF">検討した仕組み</a></h1> <p>マイブームというわけではないのですが、個人開発ではDBレスのサービスを作ることが多いです。スキーマの設計が面倒なのと個人でデータを持ちたくないのが....。</p> <h2 id="最近作った主なDBレスサービス"><a href="#%E6%9C%80%E8%BF%91%E4%BD%9C%E3%81%A3%E3%81%9F%E4%B8%BB%E3%81%AADB%E3%83%AC%E3%82%B9%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">最近作った主なDBレスサービス</a></h2> <ul> <li>キャップ野球・全国大会非公式特設サイト</li> <li>ドラフト風画像作成サービス「ドラフトなう!」</li> <li>アニメランキング作成サービス「Annict Access 3」</li> <li>音楽ランキングメーカー</li> <li>優勝ラインシミュレーター</li> <li>ボウリング幹事アプリ「bowling party manager」</li> </ul> <h2 id="timetree"><a href="#timetree">timetree</a></h2> <p>カレンダー共有ということで一番最初に考慮したのがtimetree。<br /> ただし、<a target="_blank" rel="nofollow noopener" href="https://developers.timetreeapp.com/ja/docs/api">外部公開されているAPI</a>の中にカレンダーに登録されているイベントの一覧を取得するAPIがないことがわかり(19/12/7時点)、採用を断念しました。</p> <h2 id="google calendar API"><a href="#google+calendar+API">google calendar API</a></h2> <p>定番のカレンダー。ただし、現在公開されているv3 APIはOAuth必須となっている。極力ユーザに余計なアクションをさせたくなかったので選択肢から外しました。</p> <h2 id="google public calendar"><a href="#google+public+calendar">google public calendar</a></h2> <p>googleカレンダーには誰でも閲覧できる「公開カレンダー」があり、<br /> その仕組みを調べていたところ、OAuthなしでデータを取得できるという結論に達しました。</p> <h1 id="イベントの種類タグ"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E7%A8%AE%E9%A1%9E%E3%82%BF%E3%82%B0">イベントの種類タグ</a></h1> <p>イベントの種類を分類するために、イベントのタイトルにタグをつけてもらう方式にしました。<br /> 例:<code>【イベントの種類】【イベントの場所】タイトル</code></p> <h2 id="強敵、正規表現...."><a href="#%E5%BC%B7%E6%95%B5%E3%80%81%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE....">強敵、正規表現....</a></h2> <p>エンジニアとして避けては通れない道なのはわかっているのですが、1つめの【】の中は取得できるものの、繰り返しの取得ができず、</p> <p>(ノ`Д)ノ彡 ┻━┻ ←こんな感じになりました</p> <p>結局文字列を再帰的に検索するという力業で解決しました。<br /> 文字列処理はHSP時代に腐るほどやったんや....</p> <pre><code class="javascript">findTag=(str,tags)=>{ console.log(tags) let beginIdx = str.indexOf('【'); let endIdx = str.indexOf('】'); if(beginIdx !== -1 && endIdx !== -1){ let tagStr = str.substring(beginIdx + 1,endIdx); tags.push(tagStr); this.findTag(str.substring(endIdx + 1,str.length),tags); return tags; }else{ return tags; } } </code></pre> <p>ロースキルでごめんなさい。</p> <h1 id="採用フォント"><a href="#%E6%8E%A1%E7%94%A8%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88">採用フォント</a></h1> <p><a href="https://crieit.now.sh/upload_images/6845d2efb6214962dccd0f411720e2585debc2bd16cfe.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6845d2efb6214962dccd0f411720e2585debc2bd16cfe.png?mw=700" alt="ELLZKugUwAAB9xs.png" /></a></p> <p>普段Noto Sans JPを好んで使っているのですが、Noto Sans JPのままでは若干固いイメージだったので、<code>M PLUS Rounded 1c</code>を使っています。</p> <h1 id="動作イメージ"><a href="#%E5%8B%95%E4%BD%9C%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">動作イメージ</a></h1> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/6xYVAQPyqwY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h1 id="未実装の機能"><a href="#%E6%9C%AA%E5%AE%9F%E8%A3%85%E3%81%AE%E6%A9%9F%E8%83%BD">未実装の機能</a></h1> <ul> <li>更新日時順表示</li> </ul> ckoshien tag:crieit.net,2005:PublicArticle/15573 2019-12-02T07:00:07+09:00 2019-12-02T07:00:07+09:00 https://crieit.net/posts/d2e5218181c66086dd0e423f1bc427a9 ボウリング幹事アプリを作りました。 <p>この記事は<a href="https://crieit.net/advent-calendars/2019/crieit">なんでも Advent Calendar 2019</a>の2日目の記事です。<br /> 昨日はだら@Crieit開発者さんの「<a href="https://crieit.net/posts/688d4075e654935779abc887cfe485aa">お金も名声も得ることを考えなかったら何を作りたいんだろう?</a>」という記事でした。</p> <h1 id="ボウリング幹事アプリを作りました"><a href="#%E3%83%9C%E3%82%A6%E3%83%AA%E3%83%B3%E3%82%B0%E5%B9%B9%E4%BA%8B%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F">ボウリング幹事アプリを作りました</a></h1> <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dda909adf110.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dda909adf110.jpg?mw=700" alt="" /></a><br /> 練習用のレーンを用意しました。ご自由にお使いください。<br /> - <a target="_blank" rel="nofollow noopener" href="https://letsbowling.netlify.com/editor/sandbox">https://letsbowling.netlify.com/editor/sandbox</a></p> <h2 id="どんなアプリ?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%A2%E3%83%97%E3%83%AA%3F">どんなアプリ?</a></h2> <p>ボウリング大会などの幹事さん向けに、複数レーンにまたがる大会の<strong>スコアのリアルタイム共有</strong>などができるアプリです。<br /> 閲覧専用(参加者向け)と管理用(運営者向け)でURLを分けています。</p> <h3 id="URLシェア(QRコード)"><a href="#URL%E3%82%B7%E3%82%A7%E3%82%A2%28QR%E3%82%B3%E3%83%BC%E3%83%89%29">URLシェア(QRコード)</a></h3> <h3 id="選手情報入力"><a href="#%E9%81%B8%E6%89%8B%E6%83%85%E5%A0%B1%E5%85%A5%E5%8A%9B">選手情報入力</a></h3> <p>チーム名・選手名を入力できます。</p> <h3 id="各ゲームスコア入力"><a href="#%E5%90%84%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%B9%E3%82%B3%E3%82%A2%E5%85%A5%E5%8A%9B">各ゲームスコア入力</a></h3> <p>内訳は入力できないのですが、スコアを入力すると後述のソート機能を使うことができます。</p> <h3 id="ソート・HDCP算出機能"><a href="#%E3%82%BD%E3%83%BC%E3%83%88%E3%83%BBHDCP%E7%AE%97%E5%87%BA%E6%A9%9F%E8%83%BD">ソート・HDCP算出機能</a></h3> <ul> <li>合計モード<br /> 合計を算出、ソートしたいゲームにチェックを入れます。</li> <li>HDCPモード<br /> HDCP(<strong>ハンディキャップ</strong>)はチェックを入れたゲームを対象に計算され、HDCPの項で選択したゲームのスコアとハンディキャップの合計でソートします。</li> </ul> <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55ddfcf8f27bf9.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55ddfcf8f27bf9.jpg?mw=700" alt="" /></a></p> <h3 id="動作イメージ"><a href="#%E5%8B%95%E4%BD%9C%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">動作イメージ</a></h3> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/mAIHE4E44Vo" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h3 id="開発期間"><a href="#%E9%96%8B%E7%99%BA%E6%9C%9F%E9%96%93">開発期間</a></h3> <p>3週間(平日夜とか土日)</p> <h2 id="使っている技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E6%8A%80%E8%A1%93">使っている技術</a></h2> <h3 id="MQTT"><a href="#MQTT">MQTT</a></h3> <p>先日、<a href="https://crieit.net/posts/MQTT">MQTTを使ってみよう</a>という記事を書きましたが、このWEBアプリの目玉の技術です。<br /> このアプリは<strong>DBサーバなしで動いています</strong>が、MQTTでメッセージを送受信して複数人で同じデータ(ローカルストレージ)を共有することによって画面共有を実現しています。</p> <p>DBサーバ立てるの面倒だな、ローカルストレージを共有する方法ないかな、と思ってたどり着いたのがこれ。後はWebRTCやWebSocketは候補にありました。</p> <p>複数台(PC/モバイル)で同じ画面を見ながら操作していただくとわかると思いますが、表示を同期することができます。<br /> これをサーバ(DB/AP)への問い合わせなしで実現しました。<br /> <a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55de2900126be6.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55de2900126be6.jpg?mw=700" alt="" /></a></p> <h3 id="ReactJS"><a href="#ReactJS">ReactJS</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/atlassian/react-beautiful-dnd">react-beautiful-dnd</a><br /> ドラッグアンドドロップ</li> <li><a target="_blank" rel="nofollow noopener" href="http://igorprado.com/react-notification-system/">react-notification-system</a><br /> メッセージ通知</li> </ul> <h3 id="local storage"><a href="#local+storage">local storage</a></h3> <p>データの保存に各端末のローカルストレージを使っています。</p> <h1 id="おわりに、今年リリースしたもの"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB%E3%80%81%E4%BB%8A%E5%B9%B4%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%9F%E3%82%82%E3%81%AE">おわりに、今年リリースしたもの</a></h1> <ul> <li>08/18リリース:<a href="https://crieit.net/posts/cb42a72a9e70af9ea472af216219e0e8">タイピングスコア管理システム</a></li> <li>08/21リリース:<a href="https://crieit.net/boards/baseball-score-management/f51b909e24c84342ac946117b65d092e">キャップ野球・全国大会非公式特設サイト</a></li> <li>10/06リリース:<a href="https://crieit.net/boards/baseball-score-management/JCBL-SCORE-ver3">野球リーグスコア管理システム ver3α</a></li> <li>10/21リリース:<a href="https://crieit.net/posts/4b0c9a1ddc05a0cc0ff61db75530b9b0">ドラフト風画像作成サービス「ドラフトなう!」</a></li> <li>10/27リリース:<a href="https://crieit.net/posts/AnnictAccess3">アニメランキング作成サービス「Annict Access 3」</a></li> <li>11/04リリース:<a href="https://crieit.net/boards/baseball-score-management/ReactNative">野球リーグandroidアプリ</a></li> <li>11/17リリース:<a href="https://crieit.net/posts/509e31a0bbc4ffe7a1003278816c9e5d">音楽ランキングメーカー</a></li> <li>12/01リリース:<a href="https://crieit.net/boards/baseball-score-management/d3a03b2eecede278f5eb211aaff9a6a2">優勝ラインシミュレーター</a></li> <li>12/02リリース:<strong>ボウリング幹事アプリ「bowling party manager」</strong></li> </ul> ckoshien tag:crieit.net,2005:PublicArticle/15557 2019-11-21T23:48:51+09:00 2019-11-21T23:48:51+09:00 https://crieit.net/posts/MQTT MQTTを使ってみよう <h1 id="MQTT(MQ Telemetry Transport)とは?"><a href="#MQTT%28MQ+Telemetry+Transport%29%E3%81%A8%E3%81%AF%EF%BC%9F">MQTT(MQ Telemetry Transport)とは?</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/MQ_Telemetry_Transport">wikipedia</a></li> </ul> <p>通信プロトコルの一種でPub/Sub型のデータ配信モデル。<br /> 軽量プロトコルのため、主にIoTの分野で使われていることが多いです。</p> <h2 id="Pub/Sub型"><a href="#Pub%2FSub%E5%9E%8B">Pub/Sub型</a></h2> <p>Pub/Sub型に関わるのは次の3者。</p> <ul> <li>Publisher メッセージを出す人</li> <li>Subscriber メッセージを読む人</li> <li>Broker 配送業者(中継サーバ)</li> </ul> <p>履歴の残らないLINE、という感じでしょうか。</p> <h1 id="中継サーバを立ててみよう"><a href="#%E4%B8%AD%E7%B6%99%E3%82%B5%E3%83%BC%E3%83%90%E3%82%92%E7%AB%8B%E3%81%A6%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86">中継サーバを立ててみよう</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/_/eclipse-mosquitto">eclipse-mosquitto</a>というMQTTサーバのdockerイメージが配布されています。</p> <p>今回はホストの1883番ポートにmosquittoサーバを公開します。<br /> さらにリバースプロキシの後ろに配置するため、nginxと同じnetwork(ここでは<strong>default</strong>)に接続します。</p> <p><strong>docker-compose.yml(抜粋)</strong></p> <pre><code class="yml"> mosquitto: image: eclipse-mosquitto hostname: mosquitto container_name: mosquitto ports: - "1883:1883" volumes: - ./mosquitto.conf:/mosquitto/config/mosquitto.conf networks: - default </code></pre> <p><strong>moquitto.conf(一部)</strong></p> <pre><code class="conf"># ======================================================== # Default listener # ======================================================== # Port to use for the default listener. port 1883 # Choose the protocol to use when listening. protocol websockets # listener port-number [ip address/host name] listener 9001 </code></pre> <p><strong>nginx.conf(listen 443)</strong></p> <pre><code> location /mqtt { proxy_pass http://mosquitto:1883/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } </code></pre> <h1 id="クライアント(react)側実装"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%28react%29%E5%81%B4%E5%AE%9F%E8%A3%85">クライアント(react)側実装</a></h1> <pre><code class="javascript">import PahoMQTT from "paho-mqtt"; const uuidv4 = require('uuid/v4'); //wssを利用する const client = new PahoMQTT.Client( '******',443,uuidv4() ); export const connect=()=>{ client.connect({ userName: "******", password: "********", useSSL: true, onSuccess, onFailure }); client.onMessageArrived = onMessageArrived; client.onConnectionLost = onConnectionLost; } const onSuccess=()=>{ client.subscribe(TOPIC); } const onFailure=()=>{ console.log('connect failed.') } export const onMessageArrived=(message)=>{ console.log(message.payloadString); } export const send=(message)=>{ client.send(TOPIC, message, 0, false); } function onConnectionLost(responseObject) { if (responseObject.errorCode !== 0) { connect(); } } </code></pre> <h1 id="アドベントカレンダー"><a href="#%E3%82%A2%E3%83%89%E3%83%99%E3%83%B3%E3%83%88%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC">アドベントカレンダー</a></h1> <ul> <li><a href="https://crieit.net/advent-calendars/2019/crieit">crieitアドベントカレンダー「なんでも」</a></li> </ul> <p>2日目に登板予定です。<br /> MQTTを基礎技術に使ったアプリを作る予定です。</p> ckoshien tag:crieit.net,2005:PublicArticle/15552 2019-11-17T01:32:03+09:00 2019-11-17T01:32:03+09:00 https://crieit.net/posts/509e31a0bbc4ffe7a1003278816c9e5d 音楽ランキングメーカーをリリースしました。 <p><a href="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dd018433068a.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cebc8b53a7a1a70fcf9eb56cb0dfce15dd018433068a.jpg?mw=700" alt="" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://music-ranking-maker.netlify.com/">https://music-ranking-maker.netlify.com/</a><br /> - <a target="_blank" rel="nofollow noopener" href="https://music-ranking-maker.netlify.com/replay?W3sidHJhY2tJZCI6ODY4NjI2NDQyfSx7InRyYWNrSWQiOjEwNzg4MTc0Nzd9LHsidHJhY2tJZCI6MTIyNjM0Mzg5M30seyJ0cmFja0lkIjozNTkwNzA0ODZ9LHsidHJhY2tJZCI6MzQ1NTQwNTY0fSx7InRyYWNrSWQiOjYzMzg3OTU1MH0seyJ0cmFja0lkIjoxMDU4MjI0MzUyfV0=">URLシェアの例</a></p> <h2 id="開発のきっかけ"><a href="#%E9%96%8B%E7%99%BA%E3%81%AE%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">開発のきっかけ</a></h2> <p>UIが似ているのでお気づきの方もいらっしゃると思いますが、10/27にリリースした<a href="https://crieit.net/posts/AnnictAccess3">アニメランキング作成サービス</a>の派生プロジェクトです。<br /> 楽曲の情報も持てないかな、と思ってちょっと調べたところ、<a target="_blank" rel="nofollow noopener" href="https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/">itunes search API</a>を見つけました。</p> <h2 id="動作イメージ"><a href="#%E5%8B%95%E4%BD%9C%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">動作イメージ</a></h2> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/nQbr21xa1_s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h2 id="アニメランキング作成サービスとの違い"><a href="#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%A9%E3%83%B3%E3%82%AD%E3%83%B3%E3%82%B0%E4%BD%9C%E6%88%90%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84">アニメランキング作成サービスとの違い</a></h2> <h3 id="楽曲試聴機能"><a href="#%E6%A5%BD%E6%9B%B2%E8%A9%A6%E8%81%B4%E6%A9%9F%E8%83%BD">楽曲試聴機能</a></h3> <p>今回使ったライブラリ:<a target="_blank" rel="nofollow noopener" href="https://github.com/CookPete/react-player">react-player</a><br /> 音楽プレイヤーを実装するにあたり、いくつかのコンポーネントを検討しましたが、最初のうち、あまりにUIが簡素(開発者が自由に作れる)で敬遠していました。</p> <p>ただ、<code>controls</code>オプションでHTML5 Audioのネイティブプレーヤーを表示できることに気づいたのでこちらを採用しました。</p> <h3 id="JSONPの解釈"><a href="#JSONP%E3%81%AE%E8%A7%A3%E9%87%88">JSONPの解釈</a></h3> <p>アニメランキング作成サービスではGraphQL APIを使用していましたが、<br /> itunes search APIはRESTでGETリクエストを送るだけです。<br /> ただし、今回はサーバ側ではなくクライアント側の実装なのでCORS問題が発生します。<br /> クエリパラメータに<code>callback</code>を指定して、<a target="_blank" rel="nofollow noopener" href="https://github.com/camsong/fetch-jsonp">fetch-jsonp</a>でJSONPを扱います。</p> <p><strong>itunes search APIへの問い合わせ。ページングとURLエンコード済み</strong></p> <pre><code class="javascript">(async()=>{ if(keyword !== undefined){ let url = 'https://itunes.apple.com/search?term='+encodeURIComponent(keyword)+'&offset='+((pageNum-1)*50)+'&limit=50&media=music&country=jp&lang=ja_jp'; let resp = await fetchJsonp(url); let response = await resp.json(); if(response !== null){ store.dispatch(loadData(response.results)) console.log(store.getState()); } } })().catch( error=>{ console.log(error); } ) </code></pre> <h3 id="要素内スクロール"><a href="#%E8%A6%81%E7%B4%A0%E5%86%85%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB">要素内スクロール</a></h3> <p>アニメランキング作成サービスではあまり使っていなかったのですが、<br /> 下部にプレイヤーが表示されるため、</p> <pre><code class="css">.scroll{ height: calc(100vh - ヘッダの高さ); over-flow: scroll; } </code></pre> <p>スクロールを実装しています。</p> <h3 id="選択時にUIがガタガタする問題への対処"><a href="#%E9%81%B8%E6%8A%9E%E6%99%82%E3%81%ABUI%E3%81%8C%E3%82%AC%E3%82%BF%E3%82%AC%E3%82%BF%E3%81%99%E3%82%8B%E5%95%8F%E9%A1%8C%E3%81%B8%E3%81%AE%E5%AF%BE%E5%87%A6">選択時にUIがガタガタする問題への対処</a></h3> <p>アニメランキング作成サービスではまだ改善できていませんが、<br /> 選択前と選択後で枠線の太さを変える実装をしているため、UIががたつくという問題がありました。<br /> あらかじめ同じ太さで目立たない色の枠線にしておき、選択された際に色のみを変える方法を取っています。</p> ckoshien tag:crieit.net,2005:PublicArticle/15511 2019-10-27T21:29:52+09:00 2019-10-27T21:29:52+09:00 https://crieit.net/posts/AnnictAccess3 AnnictAccess3をリニューアルしました! <p>進捗的には<a href="https://crieit.net/boards/annict-access/b2e0256fcd24966cd3ace9d0e02910c1">この記事</a>の続きなのですが、大幅にリニューアルを敢行しました。</p> <h1 id="サービスURL"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9URL">サービスURL</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://annictaccessv3.netlify.com/">https://annictaccessv3.netlify.com/</a></p> <h1 id="動作イメージ"><a href="#%E5%8B%95%E4%BD%9C%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">動作イメージ</a></h1> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/wtPe0MQl7t4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h1 id="今回の機能改善点"><a href="#%E4%BB%8A%E5%9B%9E%E3%81%AE%E6%A9%9F%E8%83%BD%E6%94%B9%E5%96%84%E7%82%B9">今回の機能改善点</a></h1> <h2 id="「アニメ選択」画面の実装"><a href="#%E3%80%8C%E3%82%A2%E3%83%8B%E3%83%A1%E9%81%B8%E6%8A%9E%E3%80%8D%E7%94%BB%E9%9D%A2%E3%81%AE%E5%AE%9F%E8%A3%85">「アニメ選択」画面の実装</a></h2> <p><a href="https://crieit.now.sh/upload_images/f2fa429ca90221dc87d712c2a35832965db586d228738.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f2fa429ca90221dc87d712c2a35832965db586d228738.jpg?mw=700" alt="" /></a></p> <ul> <li>ランキングに追加中の番組の強調</li> <li>年を指定して番組一覧を表示</li> </ul> <p>以前のバージョンでは全体から上位30位までを固定で抜き出してそれをソートする形でした。</p> <h2 id="視聴者数の表示"><a href="#%E8%A6%96%E8%81%B4%E8%80%85%E6%95%B0%E3%81%AE%E8%A1%A8%E7%A4%BA">視聴者数の表示</a></h2> <p><a href="https://crieit.now.sh/upload_images/b9eb3a9f22cb008ad6a5a6a394a24e4b5db58362b83ba.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b9eb3a9f22cb008ad6a5a6a394a24e4b5db58362b83ba.jpg?mw=700" alt="" /></a></p> <h1 id="今回苦労したこと"><a href="#%E4%BB%8A%E5%9B%9E%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">今回苦労したこと</a></h1> <h2 id="画像取得に制限がある問題"><a href="#%E7%94%BB%E5%83%8F%E5%8F%96%E5%BE%97%E3%81%AB%E5%88%B6%E9%99%90%E3%81%8C%E3%81%82%E3%82%8B%E5%95%8F%E9%A1%8C">画像取得に制限がある問題</a></h2> <ul> <li>取得数を制限する<br /> →外部APIを探す<br /> →herokuにキャッシュさせる処理を書く</li> </ul> <pre><code class="javascript"> if(isExistFile('./public/'+req.params.url +'.jpg')){ res.sendfile('./public/'+req.params.url +'.jpg'); } try { client.get('users/show',{screen_name: req.params.url}, async(error, tweets, response)=>{ if(error) return Promise.reject(error); console.log(response.statusCode) if(response.statusCode===200){ var json = JSON.parse(response.toJSON().body) let imageRes = await fetch(json.profile_image_url); downloadFile(json.profile_image_url,'./public/'+req.params.url+'.jpg') res.redirect(json.profile_image_url); } }) } catch (error) { console.error(error) } </code></pre> <h1 id="未実装の機能"><a href="#%E6%9C%AA%E5%AE%9F%E8%A3%85%E3%81%AE%E6%A9%9F%E8%83%BD">未実装の機能</a></h1> <ul> <li>映画版ランキングメーカー <ul> <li>the movie db APIからデータを取得する準備中です。</li> <li>日本語/英語両対応するかも?</li> </ul></li> <li>twitterシェア機能</li> </ul> <h1 id="既知の不具合"><a href="#%E6%97%A2%E7%9F%A5%E3%81%AE%E4%B8%8D%E5%85%B7%E5%90%88">既知の不具合</a></h1> <ul> <li>画像が読み込めなかった場合、クリックできる範囲が狭くなる</li> <li>画像取得の際にtwitterAPIの制限に引っ掛かる</li> </ul> <h1 id="今回使っている技術"><a href="#%E4%BB%8A%E5%9B%9E%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E6%8A%80%E8%A1%93">今回使っている技術</a></h1> <h2 id="ReactJS/Netlify"><a href="#ReactJS%2FNetlify">ReactJS/Netlify</a></h2> <p>ReactJS...フロントエンドの画面や通信を司るjavascriptの<code>フレームワーク</code><br /> Netlify...Reactアプリなどを簡単にデプロイできる<code>プラットフォームサービス</code></p> <h2 id="NodeJS/Heroku"><a href="#NodeJS%2FHeroku">NodeJS/Heroku</a></h2> <p>NodeJS...サーバサイドjavascript。<br /> Heroku...NodeJSやRailsなどサーバサイドアプリケーションをデプロイできる<code>プラットフォームサービス</code></p> <h2 id="graphQL"><a href="#graphQL">graphQL</a></h2> <p>サーバにデータを問い合わせる<code>クエリ言語</code>の一つ。<br /> REST APIの後継と言われている。</p> <pre><code class="javascript"> let query = gql`query { searchWorks(orderBy: {field: WATCHERS_COUNT, direction: DESC} seasons: ["${req.params.year}-spring","${req.params.year}-summer","${req.params.year}-autumn","${req.params.year}-winter"]) { edges { node { title seasonName seasonYear annictId twitterUsername watchersCount } } } }` </code></pre> ckoshien tag:crieit.net,2005:PublicArticle/15496 2019-10-21T09:22:17+09:00 2019-10-21T09:22:17+09:00 https://crieit.net/posts/4b0c9a1ddc05a0cc0ff61db75530b9b0 ドラフト風画像作成サービス「ドラフトなう!」を作りました <p><a href="https://crieit.now.sh/upload_images/c92cd6ef54296b6dfb31726f0b708d715dacf6b4b06a6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c92cd6ef54296b6dfb31726f0b708d715dacf6b4b06a6.png?mw=700" alt="EHAEyHXU0AA3c5Q.png" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://draft-now.netlify.com/">ドラフト風画像作成サービス「ドラフトなう!」</a></p> <h1 id="「ドラフトなう」とは"><a href="#%E3%80%8C%E3%83%89%E3%83%A9%E3%83%95%E3%83%88%E3%81%AA%E3%81%86%E3%80%8D%E3%81%A8%E3%81%AF">「ドラフトなう」とは</a></h1> <p><strong>ド ラ フ ト 会 議 が 終 わ っ て し ま い ま し た が、</strong><br /> フォームに必要な項目を埋めるとドラフト会議風の画面が作れます。<br /> 本家リポビタンDのサイトは12球団からしか選べないのでオリジナル球団から選べるといいなと思って作りました。</p> <p>今回はキャンバスを使って画像化までは時間がなくて実装できなかったので、<br /> 現状スクリーンショットを撮って楽しんでいただければ幸いです。</p> <h1 id="製作に関して"><a href="#%E8%A3%BD%E4%BD%9C%E3%81%AB%E9%96%A2%E3%81%97%E3%81%A6">製作に関して</a></h1> <h2 id="使った技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">使った技術</a></h2> <ul> <li>ReactJS</li> <li>CreateReactApp</li> </ul> <h2 id="製作期間"><a href="#%E8%A3%BD%E4%BD%9C%E6%9C%9F%E9%96%93">製作期間</a></h2> <ul> <li>2019/10/16(ドラフト会議前日) <ul> <li>仮リリース</li> </ul></li> <li>2019/10/17(ドラフト会議終了後) <ul> <li>レスポンシブ対応</li> </ul></li> </ul> <h2 id="苦労した点"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E7%82%B9">苦労した点</a></h2> <ul> <li>仮リリースの段階では背景に作った画像を使っていたが、レスポンシブで意図した通りに拡大縮小できなかったため、スタイルで全て書き直した。</li> <li>レスポンシブの実装をメディアクエリを使って行なったが、境界部分でUI崩れがなかなか解消できずに苦労した。</li> <li>ブランドロゴはなかなかうまくできたと思ったが、これもレスポンシブ対応が厳しく、最終的に画像化した。</li> </ul> <h1 id="教訓"><a href="#%E6%95%99%E8%A8%93">教訓</a></h1> <p>季節系のイベントサービスを作る時は事前にきちんと計画を立てましょう。<br /> <strong>前日の朝にこれ作りたいなと思って案の定間に合いませんでした。</strong></p> <p>レスポンシブ対応がドラフト会議終了後の実装になってしまったため、<br /> モバイルユーザがUI崩れでサイトを離れてしまった可能性がありました。</p> <h2 id="今回間に合わなかった機能"><a href="#%E4%BB%8A%E5%9B%9E%E9%96%93%E3%81%AB%E5%90%88%E3%82%8F%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E6%A9%9F%E8%83%BD">今回間に合わなかった機能</a></h2> <ul> <li>OGP芸 <ul> <li>バックエンドサーバを用意していないので、技術的に難しいかも。</li> </ul></li> <li>画像化(Canvas描画)</li> <li>SNSシェア機能</li> </ul> ckoshien