Author SHA1 Message Date
eai04191 f4860de95b デモ 2019-02-25 01:57:22 +09:00
eai04191 dc98334a6d cardをコンポーネント化 2019-02-23 22:29:53 +09:00
shibafuandGitHub 32b0b76032 Merge pull request #104 from hinaloe/test/fix-crlf
テスト用ファイルの改行コードなおした
2019-02-18 20:14:31 +09:00
hina db3ba04091 立つ鳥跡を濁さず
モックを使用しない場合のsleepを復活

close #102
2019-02-18 17:33:52 +09:00
hina 0400bc771c gitに改行コード変えられてた 2019-02-18 17:32:09 +09:00
shibafuandGitHub a5b4eeee36 Merge pull request #99 from shikorism/fix/68-paginator
ページャーの改善
2019-02-17 20:41:46 +09:00
shibafuandGitHub f760ea7093 Merge pull request #101 from hinaloe/tests/mockable-resolver
GuzzleHttpをモック可能にする
2019-02-17 19:36:56 +09:00
shibafu 0f4dfcd816 全ページ数にはレイアウト調整犠牲になってもらう
* .page-linkと同様にline-heightを設定
* 高さ調整のためにheightを設定
2019-02-17 11:13:12 +09:00
hina a934a7fc35 リモートURLをリクエストするテストをトグれるモックに差し替える 2019-02-17 03:19:27 +09:00
hina 51f097fdf0 最初にやったニジエに抜けがあった 2019-02-17 03:18:59 +09:00
hina 24dee801ad Guzzle\Clientをモッカブルにする 2019-02-17 02:58:36 +09:00
hina 9f1cd607d7 ignore ide-helper files 2019-02-17 02:51:52 +09:00
hina 4196b1a02d Add barryvdh/laravel-ide-helper 2019-02-17 02:51:27 +09:00
Eaiandshibafu 35789befc5 pageが反映されていない問題を修正 (#98) 2019-02-17 00:24:25 +09:00
shibafu 32139cb9da ページネーションの自前HTMLを排除 2019-02-16 23:26:09 +09:00
shibafu 9244b8424d 最終ページを出してみる 2019-02-16 23:06:58 +09:00
shibafu 55eb95dda8 スマホ向けページャーでページジャンプを可能にした 2019-02-16 23:03:21 +09:00
shibafu 72e9d4e3e8 スマートフォン向けの簡易なページャーを用意 2019-02-16 23:03:19 +09:00
shibafu 852f1ac88c Bootstrap4用のページネーションテンプレートをpublish 2019-02-16 20:08:28 +09:00
shibafuandGitHub 33be0ac8ef CienResolverを追加 (#95) 2019-02-16 00:06:51 +09:00
shibafuandGitHub 9f2e73e511 トップページに表示されるグラフが少なすぎるバグの修正 (#94) 2019-02-12 23:46:20 +09:00
shibafuandGitHub 09bb98876c Merge pull request #80 from shikorism/feature/noscript-info
ブラウザ設定でJavaScriptが無効になっている場合、メッセージを表示
2019-02-12 22:55:45 +09:00
Eaiandshibafu cedee0a20e pixivのOGP画像で常にプロキシURLを使用する (#92) 2019-02-12 22:48:44 +09:00
unaristandshibafu 116dd3b798 OGPResolverでmeta[name="description"] にフォールバックする🐯 (#91) 2019-02-12 22:45:51 +09:00
unaristandshibafu 735bb00eba 著作権表示の年を更新 (#90) 2019-02-11 23:44:07 +09:00
shibafu decb1707f1 PHP 7.0はオワコン 2019-02-11 13:18:27 +09:00
shibafuandGitHub bec7bdeb36 Merge pull request #89 from shikorism/feature/home-v2
ホーム画面の微リニューアル
2019-02-11 13:17:34 +09:00
shibafuandGitHub 7f5a4a06d9 Merge pull request #86 from unarist/feat/activitypub
ActivityPubResolverを追加
2019-02-11 13:16:29 +09:00
unarist d7c7f86ba5 CWもdescriptionに入れる 2019-02-11 03:07:52 +09:00
unarist ca212b547a Acceptに*/*を入れると無視されるので入れないようにする 2019-02-11 03:07:52 +09:00
unarist 3584625b47 Actorの取得失敗をちゃんと無視するように 2019-02-11 03:07:52 +09:00
unarist 1ba4999a83 不明なContentTypeや406はOGPにフォールバックする 2019-02-11 03:07:52 +09:00
shibafu 03e1c2d60c カード情報のリクエスト処理を共通化 2019-02-10 20:48:16 +09:00
shibafu ab0695ee8d xdebug.remote_autostartを使うと、デバッガーが不要な場合の処理時間が長くなりすぎるので止める 2019-02-10 18:58:58 +09:00
shibafu fcafc3c704 レビュー内容を部分的に反映
* Bootstrapのスタイルに乗せた
* 問題解決の方法を案内
* fixedに関わりたくなくなってきたので単にページ先頭要素にした
2019-02-10 17:00:35 +09:00
shibafu 7a606be3ba 別にIIFEである必要性ないじゃん 2019-02-09 23:38:53 +09:00
shibafu b4a7ec64dd デザインの微調整 2019-02-09 23:16:03 +09:00
unarist 5750eeb3a5 ActivityPubResolverを追加 2019-02-09 04:04:41 +09:00
shibafu b4dc07a9a3 サーバ内全体の日別総チェックイン数グラフをトップに設置 2019-02-09 02:34:53 +09:00
shibafu a77ac3f039 お惣菜コーナーのカード枠を削除
ユーザータイムラインに合わせた形
2019-02-09 00:50:47 +09:00
shibafu eef0eac887 サイトからのお知らせを左カラムに追いやった 2019-02-09 00:17:12 +09:00
Eaiandshibafu 7337f60491 お知らせがpinnedの場合ピン留めの表示をする (#58) 2019-02-08 23:45:25 +09:00
shibafuandGitHub 85e9599654 Merge pull request #85 from shikorism/feature/profile
プロフィール設定機能の強化 (自己紹介文, URL)
2019-02-07 21:36:04 +09:00
shibafuandGitHub 8a919ca62a 新規登録時に、Gravatarの利用についての案内を表示 (#84) 2019-02-07 21:35:09 +09:00
shibafuandGitHub d105568c76 検索画面のページネーションURLに検索クエリが含まれるよう修正 (#83)
refs #82
2019-02-07 21:34:57 +09:00
shibafu 1f7723614d ホームで表示する自身の情報と、ユーザーページで表示するプロフィール情報を別のものにした 2019-02-07 00:52:44 +09:00
shibafu 41e810c788 プロフィール設定画面に自己紹介とURLを追加 2019-02-07 00:52:44 +09:00
shibafu 82af423c57 プロフィール欄に自己紹介とURLを掲載 2019-02-07 00:52:44 +09:00
shibafu 4346e1a701 usersテーブルに自己紹介とURLの列を追加 2019-02-07 00:52:44 +09:00
Eaiandshibafu 96199c9e46 年齢確認の画面でその他リンクをクリックできないように修正 (#81) 2019-02-05 22:33:11 +09:00
shibafuandGitHub 4962244969 Merge pull request #77 from shikorism/feature/narou-resolver
NarouResolverを追加
2019-02-05 22:31:09 +09:00
unaristandshibafu c417dabff2 Pixivのmode=mangaに対応 (#79) 2019-02-04 02:43:40 +09:00
shibafu d9bf673d85 作者名とあらすじの取得を試みる 2019-02-04 02:35:50 +09:00
shibafu 5961d3e27a ブラウザ設定でJavaScriptが無効になっている場合、メッセージを表示 2019-02-04 00:13:49 +09:00
unaristandshibafu b57a272611 非ログイン時は年齢確認を表示するように (#78) 2019-02-04 00:10:14 +09:00
shibafuandGitHub cbbb2605dd Merge pull request #38 from eai04191/feature/profile-card
アイコンの横に名前が来るようにする
2019-02-03 21:58:01 +09:00
Eaiandshibafu e20bb75e00 PatreonResolverを追加 (#51) 2019-02-03 20:35:59 +09:00
eai04191 e226f43265 home でのみ省略表示するようにした 2019-02-03 16:47:16 +09:00
shibafu e887f2d83e NarouResolverを追加
refs #70
2019-02-03 01:10:29 +09:00
shibafuandGitHub 1835776a9c ログイン済の状態でログインページ等にアクセスした際のリダイレクト先が間違っていたので修正 (#75)
refs #69
2019-01-31 00:45:16 +09:00
shibafuandGitHub 57715b9a82 タグ入力欄の高さが自動で拡張されるよう修正 (#74)
refs #71
2019-01-31 00:35:33 +09:00
shibafuandGitHub e320f85c73 月間グラフの表示範囲が月末になると1ヶ月ズレるバグの修正 (#73)
refs #72
2019-01-31 00:14:26 +09:00
shibafu 4ab82ff0e2 Revert "Merge pull request #30 from eai04191/feature-KomifloResolver"
This reverts commit b6bf1f99d8, reversing
changes made to ef563f8641.
2019-01-23 00:42:41 +09:00
shibafuandGitHub 3c6f802b69 Merge pull request #41 from eai04191/feature/link-icon-flexbox
リンクアイコンの親要素をflexboxにしてリンクがアイコンから落ちないようにする
2019-01-23 00:21:32 +09:00
shibafuandGitHub 895e9f4b15 OGPからタイトル情報が取れない場合はtitleタグから取得を試みる (#65)
refs #62
2019-01-23 00:04:10 +09:00
shibafuandGitHub 648e171a57 Shift_JIS, EUC-JPでエンコードされたページのOGP取得対応 (#64)
refs #61
2019-01-22 12:46:44 +09:00
shibafuandGitHub dc91180dd4 チェックイン要素の全体にword-wrapを設定 (#63)
refs #28
2019-01-22 12:46:25 +09:00
Eaiandshibafu bbbffcb39e DeviantArtResolverを追加 (#52) 2019-01-19 16:19:34 +09:00
shibafu 56831c78c3 Reformat time! 2019-01-19 03:02:37 +09:00
shibafu a30919991c "composer fix" の追加 2019-01-19 03:02:30 +09:00
shibafuandGitHub 8aa2e6a779 Merge pull request #60 from shikorism/fix/56-broken-normalize 2019-01-19 02:27:07 +09:00
shibafuandGitHub 34f45d1ce8 Merge pull request #59 from shikorism/feature/settings
ユーザー設定画面 (プロフィール, プライバシー関連)
2019-01-19 02:26:35 +09:00
shibafu 6d66425fc9 正規化対象のURLにクエリパラメータとURLフラグメントが両方とも含まれる場合、正規化時に順序が崩れて不正なURLになってしまうバグの修正
refs #56
2019-01-19 02:18:35 +09:00
shibafu 626c85c07d URL正規化のテストを追加 2019-01-19 02:18:35 +09:00
shibafuandGitHub b6bf1f99d8 Merge pull request #30 from eai04191/feature-KomifloResolver
KomifloResolverに画像の取得を追加
2019-01-19 02:05:54 +09:00
eai04191 907eb87723 UTCのexpires_atをappのタイムゾーンに変換する
https://github.com/shikorism/tissue/pull/30#issuecomment-455174326
2019-01-19 01:41:11 +09:00
eai04191 72ec5d8d26 expires_atをCarbon化 2019-01-19 01:41:11 +09:00
eai04191 ca5be696c8 Komifloのメタデータに有効期限を設定する 2019-01-19 01:40:38 +09:00
eai04191 48ddac8c85 KomifloResolverに画像の取得を追加 2019-01-19 01:40:38 +09:00
shibafu a2580f29cc プロフィール設定とプライバシー設定を分割 2019-01-19 01:17:56 +09:00
shibafu 85cc865545 プロフィール設定の実装 2019-01-19 00:49:58 +09:00
shibafu 5d256519c6 プロフィール設定画面のページタイトルを追加 2019-01-19 00:49:58 +09:00
shibafu 53b459740f プライバシー設定の更新を実装 2019-01-19 00:49:58 +09:00
shibafu 550d897561 明日やろうは馬鹿やろう?知らねぇなぁ! 2019-01-19 00:49:53 +09:00
shibafu 27532685ba POST先ルート追加 2019-01-19 00:12:10 +09:00
shibafu 4654962aac プロフィール設定ページを作ろう 2019-01-19 00:12:10 +09:00
Eaiandshibafu ef563f8641 鍵垢だと概況データが出てこない問題を修正 (#53)
ref #50
2019-01-18 20:21:29 +09:00
Eaiandshibafu 7745b68dae php-cs-fixer のルールに single_quote を追加 (#54) 2019-01-18 20:00:47 +09:00
Eaiandshibafu e5ea0528a8 docker-compose.debug.ymlを追加 (#55)
* docker-compose.debug.ymlを追加

* README追記
2019-01-18 20:00:24 +09:00
Eaiandshibafu 4e1eec66be FanzaResolver追加 (#43)
FanzaのOGP画像は `ps.jpg` か `pr.jpg` で終わっている。 `pl.jpg` で高解像度なものが取得できる。
2019-01-18 00:16:02 +09:00
shibafu a33a0e542c Fix NijieResolverText (#42)
とりあえず通るようにしただけ
2019-01-17 23:26:35 +09:00
Eaiandshibafu 473280d9d2 Linux環境で権限がないためファイルを書き込めない問題を修正 (#49) 2019-01-17 22:12:26 +09:00
Eaiandshibafu 73c697f119 構築手順の最後に.envの読み直しを追記 (#47) 2019-01-17 18:15:37 +09:00
shibafuandGitHub 3a1dc72cf7 チェックイン修正時のバリデーションエラーで新規作成に飛ばされてしまうバグの修正 (#44)
refs #34
2019-01-17 01:01:42 +09:00
Eaiandshibafu b04f167709 パブリックタイムラインと検索画面で出るチェックインユーザーの名前をdisplay_nameにする (#39)
現状では設定できないため同じだがこちらのほうが好ましいはず。
2019-01-17 00:12:25 +09:00
shibafuandGitHub a3b328b55f メール欄のtypeをemailに変更 (#37)
* ログインフォームのメール欄のtypeをemailに変更

* 新規登録画面のメール欄のtypeをemailに変更

* パスワード再発行画面のメール欄のtypeをemailに変更

* パスワード再発行(実行)画面のメール欄のtypeをemailに変更
2019-01-17 00:00:33 +09:00
shibafu 497c19d06d パスワード再発行(実行)画面のメール欄のtypeをemailに変更 2019-01-16 23:57:30 +09:00
eai04191 ade52f40f4 パスワード再発行画面のメール欄のtypeをemailに変更 2019-01-16 23:20:19 +09:00
eai04191 c38d3fa799 新規登録画面のメール欄のtypeをemailに変更 2019-01-16 23:20:06 +09:00
shibafu 2a91aac569 chmod +x 2019-01-16 22:57:26 +09:00
eai04191 f367ec212f リンクアイコンの親要素をflexboxにしてリンクがアイコンから落ちないようにする 2019-01-16 19:17:30 +09:00
eai04191 735c7c289a Bootstrap v4.1.1 から v4.2.1 に更新 2019-01-16 16:33:00 +09:00
eai04191 cffe539832 アイコンの横に名前が来るように修正 2019-01-16 07:00:12 +09:00
eai04191 2191a96cac Bootstrap v4.1.1 から v4.2.1 に更新 2019-01-16 06:59:10 +09:00
eai04191 64f8b47ae0 ログインフォームのメール欄のtypeをemailに変更 2019-01-16 05:42:46 +09:00
shibafuandGitHub 2ca6c4c60d Dockerコンテナ内にXdebugを導入 (#33)
* コンテナにxdebugをインストール
* env fileをDockerに読ませるようにした
* 環境変数 APP_DEBUG に応じてXdebugをロードしてApacheを起動するようにした
* シェルスクリプトのWindows対策 (.gitattribute)
2019-01-16 00:42:05 +09:00
shibafu 0d4a61ef15 Merge pull request #32 from eai04191/feature-Metadata-Expires 2019-01-16 00:38:10 +09:00
shibafu cef23a64cb メタデータの再取得判定をチェックイン時の処理にも実装 2019-01-16 00:31:54 +09:00
shibafu cd26ef6236 メタデータの更新時に多重登録されないようにした 2019-01-16 00:31:54 +09:00
shibafu 810eea2a59 Metadata.expires_atをCarbon化 2019-01-16 00:31:54 +09:00
shibafu acb9b5821d expires_atに有効な値が設定されている場合のみ、メタデータの有効期限判定を行う 2019-01-16 00:31:54 +09:00
shibafu 5f01cc3430 メタデータエンティティにexpires_atプロパティを追加 2019-01-16 00:31:54 +09:00
eai04191 938a4d6957 metadataテーブルにexpires_atカラムを追加
メタデータの有効期限が現在より過去の場合、メタデータを再取得する
2019-01-15 16:51:50 +09:00
eai04191 a3813f19cf Merge remote-tracking branch 'upstream/develop' into develop 2019-01-15 00:08:40 +09:00
shibafu faf0755ebd Reformat 2019-01-15 00:05:01 +09:00
shibafu a2f797cbbe 独断と偏見のphp-cs-fixerルールを追加 2019-01-15 00:02:44 +09:00
shibafuandGitHub 8c6cc0692c Merge pull request #29 from eai04191/feature-FantiaResolver
FantiaResolverを追加
2019-01-15 00:01:52 +09:00
eai04191 dcf31865a1 拡張子がpngの場合に対応 2019-01-14 23:50:15 +09:00
eai04191 3dedb57fe4 正規表現を修正 2019-01-14 23:46:39 +09:00
eai04191 f134cbefa8 投稿に画像がない場合エラーが発生するのを修正 2019-01-14 23:08:37 +09:00
eai04191 72ab8bf101 FantiaResolverを追加 2019-01-14 22:55:01 +09:00
eai04191 11836ddd43 Merge remote-tracking branch 'upstream/develop' into develop 2019-01-14 21:38:34 +09:00
shibafuandGitHub 5c6417cdbe Merge pull request #25 from eai04191/feature-PixivResolver
PixivResolverの追加
2019-01-14 18:49:14 +09:00
mohemoheandshibafu 0e410ef342 composer installでdistから取れるようにする (#26) 2019-01-14 18:37:42 +09:00
eai04191 d6e981ac39 不要なセミコロンの削除 2019-01-14 18:35:34 +09:00
eai04191 98e933b833 変数名をcamelCase に統一 2019-01-14 18:34:33 +09:00
eai04191 6ff247acd7 proxizeを修正
- ドキュメントコメントを修正
- 引数名をより正確なものに修正
2019-01-14 18:27:21 +09:00
eai04191 2d04ed8dd7 thumbnailToMasterUrlを修正
- ドキュメントコメントを修正
- 変数名をより正確なものに修正
2019-01-14 18:27:21 +09:00
eai04191 d359a41033 メソッド名をcamelCaseに変更 2019-01-14 18:27:04 +09:00
shibafu 2299ac3fe7 開発環境向けの措置として、reCAPTCHAは任意設定とする 2019-01-14 18:03:58 +09:00
shibafu fcdb9d7aba reCAPTCHAの設置 2019-01-14 18:03:58 +09:00
shibafu e6abcc4402 依存関係にreCAPTCHA用パッケージを追加 2019-01-14 18:03:58 +09:00
eai04191 b0a7504691 Loggingの削除・メソッドの説明をまともに 2019-01-14 16:11:14 +09:00
eai04191 a645cb497f 謝辞を追加 2019-01-14 16:03:46 +09:00
eai04191 6105f6c860 PixivResolverを追加 2019-01-14 16:01:58 +09:00
Eaiandshibafu 9eb42f1991 不要なインポートの削除 (#23) 2019-01-14 14:19:43 +09:00
eai04191 dfea7f2f24 不要なインポートの削除 2019-01-14 11:29:16 +09:00
shibafuandGitHub 7441c26694 Merge pull request #22 from shikorism/develop
Release 20190114.0950
2019-01-14 09:51:33 +09:00
shibafuandGitHub 630be41833 Merge pull request #21 from eai04191/feature-DLsiteResolver
DLsiteResolverを追加
2019-01-14 09:39:58 +09:00
shibafuandGitHub c4a69cccbe Merge pull request #20 from eai04191/feature-dropdown-user-link
ドロップダウンにプロフィールへのリンクを追加
2019-01-14 08:35:54 +09:00
eai04191 cb3f060ba6 DLsiteResolverを追加 2019-01-14 04:22:40 +09:00
eai04191 4d7b70f9ad ドロップダウンにプロフィールへのリンクを追加 2019-01-14 02:16:14 +09:00
shibafuandGitHub 3bb6bf718e Merge pull request #18 from eai04191/issue-5
#17 開発環境の構築に関するメモを追加
2019-01-14 01:36:19 +09:00
eai04191 fc709a6624 READMEに開発環境の構築方法を追加 2019-01-14 01:23:19 +09:00
eai04191 8d5363e978 ComposerのためにDockerfileにgitを追加 2019-01-14 01:23:07 +09:00
eai04191 71137e9ab4 余分なスペースの削除 2019-01-14 01:02:35 +09:00
shibafuandGitHub f7a95befbe オカズリンクの上限を2000文字に拡張 (#16)
* マイグレーションのために依存関係を追加
* Ejaculations.linkのデータ型をTEXTに変更
* オカズリンクに2000文字の上限値を設定
2019-01-13 23:58:11 +09:00
shibafu 908790b53d Merge branch 'develop' 2019-01-12 00:51:18 +09:00
shibafu 20799dd757 お惣菜コーナーの表示件数を少し増やす 2019-01-12 00:14:51 +09:00
shibafu ca02f21812 チェックイン時のメタデータ取得に失敗した際、ログだけ残して終了する 2019-01-11 22:39:54 +09:00
shibafu 168ef1c5f6 チェックインページのタイトル内日付フォーマットをちょっと変更 2019-01-10 01:39:10 +09:00
shibafu 3c2e475e41 Merge branch 'develop' 2019-01-10 01:36:13 +09:00
shibafu 581c1ed952 メロンから取得したサムネイルからcensored flagらしきパラメータを外す 2019-01-10 01:29:26 +09:00
shibafu ad5fbc7ada ページタイトルを設定 2019-01-08 23:27:48 +09:00
shibafu cf1757319e Merge branch 'develop' 2019-01-05 01:05:25 +09:00
shibafuandGitHub b39b43e705 時間帯・曜日別チェックイン回数グラフ (#14)
* 曜日別回数グラフの追加
* 時間別回数グラフの追加
2019-01-02 14:44:31 +09:00
shibafu bcc9f3acda Merge branch 'develop' 2018-12-17 23:27:46 +09:00
shibafu 911957b283 nijieのview_popup.php URLからもIDが引けるので正規化して対応 2018-12-17 23:27:19 +09:00
shibafu b9e29cc283 Merge branch 'develop' 2018-12-14 00:36:58 +09:00
shibafu 316b453cca Laravel 5.5.44にアップデート
ついでにcomposer.jsonをlaravel/laravel:5.5の現状に合わせた
2018-12-14 00:25:19 +09:00
shibafu e6b28993de parsedown/laravelへの依存を削除し、同等のBladeディレクティブを移植 2018-12-13 23:49:26 +09:00
shibafu e875d5da02 Merge branch 'develop' 2018-11-20 23:33:07 +09:00
shibafu 233a54eb3e Iwaraのカード表示対応
refs #12
2018-11-20 23:31:57 +09:00
shibafu 515e24c4e4 Merge branch 'develop' 2018-09-11 23:22:29 +09:00
shibafu af4b60d6e1 LaravelドキュメントのDeploymentsに書かれているコマンドを実行するスクリプト 2018-09-11 23:19:41 +09:00
shibafu 5bb44ab232 検索ボックスにとりあえずrequiredを付けた 2018-09-11 23:10:30 +09:00
shibafu 874e88cc56 プロフィールの「よく使っているタグ」を検索リンク化 2018-09-11 23:08:11 +09:00
shibafu bdb1640ceb チェックインのタグを検索リンク化 2018-09-11 23:05:01 +09:00
shibafu c52046d51e オカズリンク入力欄のオートコンプリートを無効化 2018-09-08 00:19:50 +09:00
shibafu 69f212d705 検索ページの初期実装 2018-09-06 23:48:54 +09:00
shibafu 2441fe78b6 Merge branch 'develop' 2018-06-13 21:56:40 +09:00
shibafu 9646f90ce3 通販サイトのメタデータ対応 2018-06-13 01:00:24 +09:00
shibafu 9bd22d9f77 OGPResolverでTwitter Cardsの解析も行えるようにした (#9) 2018-06-13 01:00:11 +09:00
shibafu a0e4063c47 なんか変わってた 2018-06-13 00:25:34 +09:00
shibafu 0882063c0b ログアウトメニューを右寄せにした 2018-06-12 22:48:43 +09:00
shibafu b4e40ab748 Bootstrap 4.1にアップデート (#5) 2018-06-12 22:44:07 +09:00
shibafu 6609965360 Fix typo 2018-06-12 21:52:10 +09:00
shibafu 9705c2ce5a よく使っているタグを表示する機能 2018-06-09 00:30:41 +09:00
shibafu bd93d9ec24 varchar(255)で収まるわけないので作りなおす 2018-06-08 01:44:39 +09:00
shibafu 55bd35ea49 Merge branch 'develop' 2018-06-08 01:00:18 +09:00
shibafu 4dc4efe10d エロ漫画のタイトルくらいは取りたかった 2018-06-08 00:44:42 +09:00
shibafu dfe149e969 オカズリンクのデータを保存するようにした (#4) 2018-06-07 23:46:40 +09:00
shibafu f51aaea94c お惣菜コーナーにも「同じオカズでチェックイン」ボタンを表示する 2018-06-07 02:23:09 +09:00
shibafu 1dea0a077c 他人のチェックインにも「同じオカズでチェックイン」ボタンを表示する (#8) 2018-06-07 02:21:40 +09:00
shibafu d143dc4d84 ドッカーン 2018-06-05 23:45:29 +09:00
shibafu e033816eab Merge branch 'develop' 2018-06-02 23:33:47 +09:00
shibafu 503e8ba093 「同じオカズでチェックイン」ボタンの追加 2018-06-02 23:33:16 +09:00
shibafu d224e6bba4 gifとmp4の場合はサムネイルを取得させない 2018-04-15 23:16:19 +09:00
shibafu 3b2e81818b sp.nijie.infoの画像情報を正しく取得できるようにした
fix #7
2018-04-15 21:46:49 +09:00
shibafu 7ca0acacb4 コンテンツ情報取得の実装をapi.phpから剥がした 2018-04-15 03:29:00 +09:00
shibafu 88456bc609 Merge branch 'develop' 2018-04-11 01:18:01 +09:00
shibafu 0f39b502e8 オカズリンクのサムネイル取得にて、特定のJSONのデコード前に改行コードをエスケープするようにした 2018-04-11 01:17:52 +09:00
shibafu 46f049c2b8 Merge branch 'develop' 2018-03-06 23:50:12 +09:00
shibafu 9cdfadf12c オカズリンクのカード幅を調整
fix #3
2018-03-06 23:49:34 +09:00
shibafu bc57a482be シコ草を日曜始まりに変更 2018-03-06 23:36:45 +09:00
shibafu ba53156beb 会員登録リンクをヘッダーに追加
fix #2
2018-03-06 23:36:41 +09:00
ShibafuandGitHub 5b2427a2c9 チェックイン画面でクエリパラメータを受け付ける (#6)
* チェックイン画面でクエリパラメータを受け付けるようにした

* バリデーションエラー吐いたときにクエリパラメータが蘇らないようにした
2018-03-06 22:43:49 +09:00
shibafu 277ee90379 お惣菜コーナーのユーザーURLを正しいものに修正 2018-01-09 21:56:59 +09:00
124 changed files with 8937 additions and 14469 deletions
+4
View File
@@ -0,0 +1,4 @@
.idea
.git
.gitignore
.gitattributes
+11 -6
View File
@@ -5,12 +5,12 @@ APP_DEBUG=true
APP_LOG_LEVEL=debug APP_LOG_LEVEL=debug
APP_URL=http://localhost APP_URL=http://localhost
DB_CONNECTION=mysql DB_CONNECTION=pgsql
DB_HOST=127.0.0.1 DB_HOST=db
DB_PORT=3306 DB_PORT=5432
DB_DATABASE=homestead DB_DATABASE=tissue
DB_USERNAME=homestead DB_USERNAME=tissue
DB_PASSWORD=secret DB_PASSWORD=tissue
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
@@ -35,3 +35,8 @@ SPARKPOST_SECRET=
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
# (Optional) reCAPTCHA Key
# https://www.google.com/recaptcha
NOCAPTCHA_SECRET=
NOCAPTCHA_SITEKEY=
+1
View File
@@ -3,3 +3,4 @@
*.scss linguist-vendored *.scss linguist-vendored
*.js linguist-vendored *.js linguist-vendored
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
*.sh text eol=lf
+4
View File
@@ -11,3 +11,7 @@ npm-debug.log
yarn-error.log yarn-error.log
.env .env
*.iml *.iml
.php_cs
.php_cs.cache
.phpstorm.meta.php
_ide_helper*.php
+27
View File
@@ -0,0 +1,27 @@
<?php
return \PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'array_syntax' => [
'syntax' => 'short'
],
'blank_line_before_return' => true,
'function_typehint_space' => true,
'method_separation' => true,
'ordered_imports' => true,
'return_type_declaration' => true,
'new_with_braces' => true,
'no_empty_statement' => true,
'standardize_not_equals' => true,
'single_quote' => true
])
->setFinder(
\PhpCsFixer\Finder::create()
->exclude('bootstrap/cache')
->exclude('resources/views')
->exclude('storage')
->exclude('vendor')
->exclude('node_modules')
->in(__DIR__)
);
+21
View File
@@ -0,0 +1,21 @@
FROM php:7.1-apache
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
RUN apt-get update \
&& apt-get install -y git libpq-dev unzip \
&& docker-php-ext-install pdo_pgsql \
&& pecl install xdebug \
&& curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& a2enmod rewrite
COPY dist/bin /usr/local/bin/
COPY dist/php.d /usr/local/etc/php/php.d/
ENTRYPOINT ["tissue-entrypoint.sh"]
CMD ["apache2-foreground"]
WORKDIR /var/www/html
+62 -7
View File
@@ -1,5 +1,4 @@
Tissue # Tissue
====
a.k.a. shikorism.net a.k.a. shikorism.net
@@ -7,13 +6,69 @@ a.k.a. shikorism.net
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります) (思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
## 構成 ## 構成
* Laravel 5.5
* Bootstrap 4.0 - Laravel 5.5
- Bootstrap 4.2.1
## 実行環境 ## 実行環境
* PHP 7.1
* PostgreSQL 9.6 - PHP 7.1
- PostgreSQL 9.6
## 開発環境の構築
Docker を用いた開発環境の構築方法です。
1. `.env` ファイルを用意します。`.env.example` をコピーすることで用意ができます。
2. Docker イメージをビルドします
```
docker-compose build
```
3. Docker コンテナを起動します。
```
docker-compose up -d
```
4. Composer を使い必要なライブラリをインストールします。
```
docker-compose exec web composer install
```
5. 暗号化キーの作成と、データベースのマイグレーションを行います。
```
docker-compose exec web php artisan key:generate
docker-compose exec web php artisan migrate
```
6. ファイルに書き込めるように権限を設定します。
```
docker-compose exec web chown -R www-data /var/www/html
```
7. 最後に `.env` を読み込み直すために起動し直します。
```
docker-compose up -d
```
これで準備は完了です。Tissue が動いていれば `http://localhost:4545/` でアクセスができます。
## デバッグ実行
```
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
```
で起動することにより、DB のポート`5432`を開放してホストマシンから接続できるようになります。
## 環境構築上の諸注意 ## 環境構築上の諸注意
* 初版時点では、DBサーバとしてPostgreSQLを使うよう .env ファイルを設定するくらいです。
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。
当分、PostgreSQL から変える気はないので専用 SQL 等を平気で使います。 当分、PostgreSQL から変える気はないので専用 SQL 等を平気で使います。
+7
View File
@@ -27,4 +27,11 @@ class Ejaculation extends Model
{ {
return $this->belongsToMany('App\Tag')->withTimestamps(); return $this->belongsToMany('App\Tag')->withTimestamps();
} }
public function textTags()
{
return implode(' ', $this->tags->map(function ($v) {
return $v->name;
})->all());
}
} }
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Events;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LinkDiscovered
{
use Dispatchable, SerializesModels;
public $url;
/**
* Create a new event instance.
*
* @param string $url
*/
public function __construct(string $url)
{
$this->url = $url;
}
}
@@ -2,10 +2,10 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\User;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator; use App\User;
use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller class RegisterController extends Controller
{ {
@@ -47,11 +47,20 @@ class RegisterController extends Controller
*/ */
protected function validator(array $data) protected function validator(array $data)
{ {
return Validator::make($data, [ $rules = [
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users', 'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users',
'email' => 'required|string|email|max:255|unique:users', 'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed', 'password' => 'required|string|min:6|confirmed'
], ];
// reCAPTCHAのキーが設定されている場合、判定を有効化
if (!empty(config('captcha.secret'))) {
$rules['g-recaptcha-response'] = 'required|captcha';
}
return Validator::make(
$data,
$rules,
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'], ['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
['name' => 'ユーザー名'] ['name' => 'ユーザー名']
); );
+3 -3
View File
@@ -2,10 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController class Controller extends BaseController
{ {
+38 -10
View File
@@ -2,19 +2,29 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Ejaculation;
use App\Events\LinkDiscovered;
use App\Tag; use App\Tag;
use App\User; use App\User;
use Carbon\Carbon; use Carbon\Carbon;
use Validator;
use App\Ejaculation;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Validator;
class EjaculationController extends Controller class EjaculationController extends Controller
{ {
public function create() public function create(Request $request)
{ {
return view('ejaculation.checkin'); $defaults = [
'date' => $request->input('date', date('Y/m/d')),
'time' => $request->input('time', date('H:i')),
'link' => $request->input('link', ''),
'tags' => $request->input('tags', ''),
'note' => $request->input('note', ''),
'is_private' => $request->input('is_private', 0) == 1
];
return view('ejaculation.checkin')->with('defaults', $defaults);
} }
public function store(Request $request) public function store(Request $request)
@@ -24,11 +34,11 @@ class EjaculationController extends Controller
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']); $inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
} }
Validator::make($inputs, [ $validator = Validator::make($inputs, [
'date' => 'required|date_format:Y/m/d', 'date' => 'required|date_format:Y/m/d',
'time' => 'required|date_format:H:i', 'time' => 'required|date_format:H:i',
'note' => 'nullable|string|max:500', 'note' => 'nullable|string|max:500',
'link' => 'nullable|url', 'link' => 'nullable|url|max:2000',
'tags' => 'nullable|string', 'tags' => 'nullable|string',
])->after(function ($validator) use ($request, $inputs) { ])->after(function ($validator) use ($request, $inputs) {
// 日時の重複チェック // 日時の重複チェック
@@ -38,7 +48,11 @@ class EjaculationController extends Controller
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。'); $validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
} }
} }
})->validate(); });
if ($validator->fails()) {
return redirect()->route('checkin')->withErrors($validator)->withInput();
}
$ejaculation = Ejaculation::create([ $ejaculation = Ejaculation::create([
'user_id' => Auth::id(), 'user_id' => Auth::id(),
@@ -58,6 +72,10 @@ class EjaculationController extends Controller
} }
$ejaculation->tags()->sync($tagIds); $ejaculation->tags()->sync($tagIds);
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
}
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!'); return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
} }
@@ -86,6 +104,7 @@ class EjaculationController extends Controller
public function edit($id) public function edit($id)
{ {
$ejaculation = Ejaculation::findOrFail($id); $ejaculation = Ejaculation::findOrFail($id);
return view('ejaculation.edit')->with(compact('ejaculation')); return view('ejaculation.edit')->with(compact('ejaculation'));
} }
@@ -98,11 +117,11 @@ class EjaculationController extends Controller
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']); $inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
} }
Validator::make($inputs, [ $validator = Validator::make($inputs, [
'date' => 'required|date_format:Y/m/d', 'date' => 'required|date_format:Y/m/d',
'time' => 'required|date_format:H:i', 'time' => 'required|date_format:H:i',
'note' => 'nullable|string|max:500', 'note' => 'nullable|string|max:500',
'link' => 'nullable|url', 'link' => 'nullable|url|max:2000',
'tags' => 'nullable|string', 'tags' => 'nullable|string',
])->after(function ($validator) use ($id, $request, $inputs) { ])->after(function ($validator) use ($id, $request, $inputs) {
// 日時の重複チェック // 日時の重複チェック
@@ -112,7 +131,11 @@ class EjaculationController extends Controller
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。'); $validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
} }
} }
})->validate(); });
if ($validator->fails()) {
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
}
$ejaculation->fill([ $ejaculation->fill([
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']), 'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
@@ -131,6 +154,10 @@ class EjaculationController extends Controller
} }
$ejaculation->tags()->sync($tagIds); $ejaculation->tags()->sync($tagIds);
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
}
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!'); return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
} }
@@ -140,6 +167,7 @@ class EjaculationController extends Controller
$user = User::findOrFail($ejaculation->user_id); $user = User::findOrFail($ejaculation->user_id);
$ejaculation->tags()->detach(); $ejaculation->tags()->detach();
$ejaculation->delete(); $ejaculation->delete();
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。'); return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
} }
} }
+27 -2
View File
@@ -36,6 +36,31 @@ class HomeController extends Controller
$categories = Information::CATEGORIES; $categories = Information::CATEGORIES;
if (Auth::check()) { if (Auth::check()) {
// チェックイン動向グラフ用のデータ取得
$groupByDay = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
count(*) AS "count"
SQL
))
->join('users', function ($join) {
$join->on('users.id', '=', 'ejaculations.user_id')
->where('users.accept_analytics', true);
})
->where('ejaculated_date', '>=', now()->subDays(30))
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->get()
->mapWithKeys(function ($item) {
return [$item['date'] => $item['count']];
});
$globalEjaculationCounts = [];
$day = Carbon::now()->subDays(29);
for ($i = 0; $i < 30; $i++) {
$globalEjaculationCounts[$day->format('Y/m/d') . ' の総チェックイン数'] = $groupByDay[$day->format('Y/m/d')] ?? 0;
$day->addDay();
}
// お惣菜コーナー用のデータ取得 // お惣菜コーナー用のデータ取得
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id') $publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
->where('users.is_protected', false) ->where('users.is_protected', false)
@@ -44,10 +69,10 @@ class HomeController extends Controller
->orderBy('ejaculations.ejaculated_date', 'desc') ->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*') ->select('ejaculations.*')
->with('user', 'tags') ->with('user', 'tags')
->take(5) ->take(10)
->get(); ->get();
return view('home')->with(compact('informations', 'categories', 'publicLinkedEjaculations')); return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
} else { } else {
return view('guest')->with(compact('informations', 'categories')); return view('guest')->with(compact('informations', 'categories'));
} }
+2
View File
@@ -14,6 +14,7 @@ class InfoController extends Controller
->orderByDesc('pinned') ->orderByDesc('pinned')
->orderByDesc('created_at') ->orderByDesc('created_at')
->paginate(20); ->paginate(20);
return view('info.index')->with([ return view('info.index')->with([
'informations' => $informations, 'informations' => $informations,
'categories' => Information::CATEGORIES 'categories' => Information::CATEGORIES
@@ -23,6 +24,7 @@ class InfoController extends Controller
public function show($id) public function show($id)
{ {
$information = Information::findOrFail($id); $information = Information::findOrFail($id);
return view('info.show')->with([ return view('info.show')->with([
'info' => $information, 'info' => $information,
'category' => Information::CATEGORIES[$information->category] 'category' => Information::CATEGORIES[$information->category]
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Ejaculation;
use App\Tag;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function index(Request $request)
{
$inputs = $request->validate([
'q' => 'required'
]);
$results = Ejaculation::query()
->whereHas('tags', function ($query) use ($inputs) {
$query->where('name', 'like', "%{$inputs['q']}%");
})
->where('is_private', false)
->orderBy('ejaculated_date', 'desc')
->with(['user', 'tags'])
->paginate(20)
->appends($inputs);
return view('search.index')->with(compact('inputs', 'results'));
}
public function relatedTag(Request $request)
{
$inputs = $request->validate([
'q' => 'required'
]);
$results = Tag::query()
->where('name', 'like', "%{$inputs['q']}%")
->paginate(50)
->appends($inputs);
return view('search.relatedTag')->with(compact('inputs', 'results'));
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class SettingController extends Controller
{
public function profile()
{
return view('setting.profile');
}
public function updateProfile(Request $request)
{
$inputs = $request->all();
$validator = Validator::make($inputs, [
'display_name' => 'required|string|max:20',
'bio' => 'nullable|string|max:160',
'url' => 'nullable|url|max:2000'
], [], [
'display_name' => '名前',
'bio' => '自己紹介',
'url' => 'URL'
]);
if ($validator->fails()) {
return redirect()->route('setting')->withErrors($validator)->withInput();
}
$user = Auth::user();
$user->display_name = $inputs['display_name'];
$user->bio = $inputs['bio'] ?? '';
$user->url = $inputs['url'] ?? '';
$user->save();
return redirect()->route('setting')->with('status', 'プロフィールを更新しました。');
}
public function privacy()
{
return view('setting.privacy');
}
public function updatePrivacy(Request $request)
{
$inputs = $request->all(['is_protected', 'accept_analytics']);
$user = Auth::user();
$user->is_protected = $inputs['is_protected'] ?? false;
$user->accept_analytics = $inputs['accept_analytics'] ?? false;
$user->save();
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
}
// ( ◠‿◠ )☛ここに気づいたか・・・消えてもらう ▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああああ
// public function password()
// {
// abort(501);
// }
}
+42 -6
View File
@@ -21,7 +21,8 @@ class UserController extends Controller
} }
// チェックインの取得 // チェックインの取得
$query = Ejaculation::select(DB::raw(<<<'SQL' $query = Ejaculation::select(DB::raw(
<<<'SQL'
id, id,
ejaculated_date, ejaculated_date,
note, note,
@@ -39,7 +40,21 @@ SQL
->with('tags') ->with('tags')
->paginate(20); ->paginate(20);
return view('user.profile')->with(compact('user', 'ejaculations')); // よく使っているタグ
$tagsQuery = DB::table('ejaculations')
->join('ejaculation_tag', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id')
->join('tags', 'ejaculation_tag.tag_id', '=', 'tags.id')
->selectRaw('tags.name, count(*) as count')
->where('ejaculations.user_id', $user->id);
if (!Auth::check() || $user->id !== Auth::id()) {
$tagsQuery = $tagsQuery->where('ejaculations.is_private', false);
}
$tags = $tagsQuery->groupBy('tags.name')
->orderBy('count', 'desc')
->limit(10)
->get();
return view('user.profile')->with(compact('user', 'ejaculations', 'tags'));
} }
public function stats($name) public function stats($name)
@@ -49,7 +64,8 @@ SQL
abort(404); abort(404);
} }
$groupByDay = Ejaculation::select(DB::raw(<<<'SQL' $groupByDay = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date", to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
count(*) AS "count" count(*) AS "count"
SQL SQL
@@ -59,9 +75,22 @@ SQL
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')")) ->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->get(); ->get();
$groupByHour = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'HH24') AS "hour",
count(*) AS "count"
SQL
))
->where('user_id', $user->id)
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
->orderBy(DB::raw('1'))
->get();
$dailySum = []; $dailySum = [];
$monthlySum = []; $monthlySum = [];
$yearlySum = []; $yearlySum = [];
$dowSum = array_fill(0, 7, 0);
$hourlySum = array_fill(0, 24, 0);
// 年間グラフ用の配列初期化 // 年間グラフ用の配列初期化
if ($groupByDay->first() !== null) { if ($groupByDay->first() !== null) {
@@ -73,7 +102,7 @@ SQL
} }
// 月間グラフ用の配列初期化 // 月間グラフ用の配列初期化
$month = Carbon::now()->subMonth(11)->firstOfMonth(); // 直近12ヶ月 $month = Carbon::now()->firstOfMonth()->subMonth(11); // 直近12ヶ月
for ($i = 0; $i < 12; $i++) { for ($i = 0; $i < 12; $i++) {
$monthlySum[$month->format('Y/m')] = 0; $monthlySum[$month->format('Y/m')] = 0;
$month->addMonth(); $month->addMonth();
@@ -85,12 +114,18 @@ SQL
$dailySum[$date->timestamp] = $data->count; $dailySum[$date->timestamp] = $data->count;
$yearlySum[$date->year] += $data->count; $yearlySum[$date->year] += $data->count;
$dowSum[$date->dayOfWeek] += $data->count;
if (isset($monthlySum[$yearAndMonth])) { if (isset($monthlySum[$yearAndMonth])) {
$monthlySum[$yearAndMonth] += $data->count; $monthlySum[$yearAndMonth] += $data->count;
} }
} }
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum')); foreach ($groupByHour as $data) {
$hour = (int)$data->hour;
$hourlySum[$hour] += $data->count;
}
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum', 'dowSum', 'hourlySum'));
} }
public function okazu($name) public function okazu($name)
@@ -101,7 +136,8 @@ SQL
} }
// チェックインの取得 // チェックインの取得
$query = Ejaculation::select(DB::raw(<<<'SQL' $query = Ejaculation::select(DB::raw(
<<<'SQL'
id, id,
ejaculated_date, ejaculated_date,
note, note,
@@ -18,7 +18,7 @@ class RedirectIfAuthenticated
public function handle($request, Closure $next, $guard = null) public function handle($request, Closure $next, $guard = null)
{ {
if (Auth::guard($guard)->check()) { if (Auth::guard($guard)->check()) {
return redirect('/home'); return redirect()->route('home');
} }
return $next($request); return $next($request);
@@ -7,7 +7,7 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\View\View; use Illuminate\View\View;
class ProfileComposer class ProfileStatsComposer
{ {
public function __construct() public function __construct()
{ {
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Listeners;
use App\Events\LinkDiscovered;
use App\Metadata;
use App\MetadataResolver\MetadataResolver;
use App\Utilities\Formatter;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class LinkCollector
{
/** @var Formatter */
private $formatter;
/** @var MetadataResolver */
private $metadataResolver;
/**
* Create the event listener.
*
* @param Formatter $formatter
* @param MetadataResolver $metadataResolver
*/
public function __construct(Formatter $formatter, MetadataResolver $metadataResolver)
{
$this->formatter = $formatter;
$this->metadataResolver = $metadataResolver;
}
/**
* Handle the event.
*
* @param LinkDiscovered $event
* @return void
*/
public function handle(LinkDiscovered $event)
{
// URLの正規化
$url = $this->formatter->normalizeUrl($event->url);
// 無かったら取得
// TODO: ある程度古かったら再取得とかありだと思う
$metadata = Metadata::find($url);
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
try {
$resolved = $this->metadataResolver->resolve($url);
Metadata::updateOrCreate(['url' => $url], [
'title' => $resolved->title,
'description' => $resolved->description,
'image' => $resolved->image,
'expires_at' => $resolved->expires_at
]);
} catch (TransferException $e) {
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
report($e);
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Metadata extends Model
{
public $incrementing = false;
protected $primaryKey = 'url';
protected $keyType = 'string';
protected $fillable = ['url', 'title', 'description', 'image', 'expires_at'];
protected $visible = ['url', 'title', 'description', 'image', 'expires_at'];
protected $dates = ['created_at', 'updated_at', 'expires_at'];
}
@@ -0,0 +1,80 @@
<?php
namespace App\MetadataResolver;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Support\Facades\Log;
class ActivityPubResolver implements Resolver, Parser
{
/**
* @var \GuzzleHttp\Client
*/
private $activityClient;
public function __construct()
{
$this->activityClient = new \GuzzleHttp\Client([
'headers' => [
'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
]
]);
}
public function resolve(string $url): Metadata
{
$res = $this->activityClient->get($url);
if ($res->getStatusCode() === 200) {
return $this->parse($res->getBody());
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
public function parse(string $json): Metadata
{
$activityOrObject = json_decode($json, true);
$object = $activityOrObject['object'] ?? $activityOrObject;
$metadata = new Metadata();
$metadata->title = isset($object['attributedTo']) ? $this->getTitleFromActor($object['attributedTo']) : '';
$metadata->description .= isset($object['summary']) ? $object['summary'] . " | " : '';
$metadata->description .= isset($object['content']) ? $this->html2text($object['content']) : '';
$metadata->image = $object['attachment'][0]['url'] ?? '';
return $metadata;
}
private function getTitleFromActor(string $url): string
{
try {
$res = $this->activityClient->get($url);
if ($res->getStatusCode() !== 200) {
Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
return '';
}
$actor = json_decode($res->getBody(), true);
$title = $actor['name'] ?? '';
if (isset($actor['preferredUsername'])) {
$title .= ' (@' . $actor['preferredUsername'] . '@' . parse_url($actor['id'], PHP_URL_HOST) . ')';
}
return $title;
} catch (TransferException $e) {
Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
return '';
}
}
private function html2text(string $html): string
{
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
$html = preg_replace('~<br\s*/?\s*>|</p>\s*<p[^>]*>~i', "\n", $html);
$dom = new \DOMDocument();
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
return $dom->textContent;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\MetadataResolver;
use Carbon\Carbon;
use GuzzleHttp\Client;
class CienResolver extends MetadataResolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
// 画像URLから有効期限の起点を拾う
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $params);
if (empty($params['px-time'])) {
throw new \RuntimeException('Parameter "px-time" not found. Image=' . $metadata->image . ' Source=' . $url);
}
$metadata->expires_at = Carbon::createFromTimestamp($params['px-time'])->addHour(1);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class DLsiteResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$metadata->image = str_replace('img_sam.jpg', 'img_main.jpg', $metadata->image);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class DeviantArtResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$node = $xpath->query('//*[@id="pimp-preload"]/following-sibling::div//img')->item(0);
$srcset = $node->getAttribute('srcset');
$srcset_array = explode('w,', $srcset);
$src = end($srcset_array);
$src = preg_replace('~ \d+w$~', '', $src);
if (preg_match('~\.wixmp\.com$~', parse_url($src)['host'])) {
// アスペクト比を保ったまま、縦か横が最大700pxになるように変換する。
// Ref: https://support.wixmp.com/en/article/image-service-3835799
if (strpos($src, '/v1/fill/')) {
$src = preg_replace('~/v1/fill/w_\d+,h_\d+,q_\d+,strp~', '/v1/fit/w_700,h_700,q_70,strp', $src);
} else {
$src = $src . '/v1/fit/w_700,h_700,q_70,strp/image.jpg';
}
}
$metadata->image = $src;
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class FantiaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
preg_match("~\d+~", $url, $match);
$postId = $match[0];
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$node = $xpath->query("//meta[@property='twitter:image']")->item(0);
$ogpUrl = $node->getAttribute('content');
// 投稿に画像がない場合(ogp.jpgでない場合)のみ大きい画像に変換する
if ($ogpUrl != 'http://fantia.jp/images/ogp.jpg') {
preg_match("~https://fantia\.s3\.amazonaws\.com/uploads/post/file/{$postId}/ogp_(.*?)\.(jpg|png)~", $ogpUrl, $match);
$uuid = $match[1];
$extension = $match[2];
// 大きい画像に変換
$metadata->image = "https://c.fantia.jp/uploads/post/file/{$postId}/main_{$uuid}.{$extension}";
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class FanzaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $metadata->image);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class IwaraResolver implements Resolver
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$metadata = new Metadata();
// find title
foreach ($xpath->query('//title') as $node) {
$content = $node->textContent;
if (!empty($content)) {
$metadata->title = $content;
break;
}
}
// find thumbnail
foreach ($xpath->query('//*[@id="video-player"]') as $node) {
$poster = $node->getAttribute('poster');
if (!empty($poster)) {
if (strpos($poster, '//') === 0) {
$poster = 'https:' . $poster;
}
$metadata->image = $poster;
break;
}
}
if (empty($metadata->image)) {
// YouTube embedded?
foreach ($xpath->query('//div[@class="embedded-video"]//iframe') as $node) {
$src = $node->getAttribute('src');
if (preg_match('~youtube\.com/embed/(\S+)\?~', $src, $matches) !== -1) {
$youtubeId = $matches[1];
$iwaraThumbUrl = 'https://i.iwara.tv/sites/default/files/styles/thumbnail/public/video_embed_field_thumbnails/youtube/' . $youtubeId . '.jpg';
$metadata->image = $iwaraThumbUrl;
break;
}
}
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class KomifloResolver implements Resolver
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
if (preg_match('~komiflo\.com(?:/#!)?/comics/(\\d+)~', $url, $matches) !== 1) {
throw new \RuntimeException("Unmatched URL Pattern: $url");
}
$id = $matches[1];
$res = $this->client->get('https://api.komiflo.com/content/id/' . $id);
if ($res->getStatusCode() === 200) {
$json = json_decode($res->getBody()->getContents(), true);
$metadata = new Metadata();
$metadata->title = $json['content']['data']['title'] ?? '';
$metadata->description = ($json['content']['attributes']['artists']['children'][0]['data']['name'] ?? '?') .
' - ' .
($json['content']['parents'][0]['data']['title'] ?? '?');
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class MelonbooksResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$cookieJar = CookieJar::fromArray(['AUTH_ADULT' => '1'], 'www.melonbooks.co.jp');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
// censoredフラグの除去
if (mb_strpos($metadata->image, '&c=1') !== false) {
$metadata->image = preg_replace('/&c=1/u', '', $metadata->image);
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\MetadataResolver;
use Carbon\Carbon;
class Metadata
{
public $title = '';
public $description = '';
public $image = '';
/** @var Carbon|null */
public $expires_at = null;
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
class MetadataResolver implements Resolver
{
public $rules = [
'~(((sp\.)?seiga\.nicovideo\.jp/seiga(/#!)?|nico\.ms))/im~' => NicoSeigaResolver::class,
'~nijie\.info/view(_popup)?\.php~' => NijieResolver::class,
'~komiflo\.com(/#!)?/comics/(\\d+)~' => KomifloResolver::class,
'~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class,
'~ec\.toranoana\.jp/tora_r/ec/item/.*~' => ToranoanaResolver::class,
'~iwara\.tv/videos/.*~' => IwaraResolver::class,
'~www\.dlsite\.com/.*/work/=/product_id/..\d+\.html~' => DLsiteResolver::class,
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
'~fantia\.jp/posts/\d+~' => FantiaResolver::class,
'~dmm\.co\.jp/~' => FanzaResolver::class,
'~www\.patreon\.com/~' => PatreonResolver::class,
'~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class,
'~\.syosetu\.com/n\d+[a-z]{2,}~' => NarouResolver::class,
'~ci-en\.jp/creator/\d+/article/\d+~' => CienResolver::class,
];
public $mimeTypes = [
'application/activity+json' => ActivityPubResolver::class,
'application/ld+json' => ActivityPubResolver::class,
'text/html' => OGPResolver::class,
'*/*' => OGPResolver::class
];
public $defaultResolver = OGPResolver::class;
public function resolve(string $url): Metadata
{
foreach ($this->rules as $pattern => $class) {
if (preg_match($pattern, $url) === 1) {
/** @var Resolver $resolver */
$resolver = app($class);
return $resolver->resolve($url);
}
}
$result = $this->resolveWithAcceptHeader($url);
if ($result !== null) {
return $result;
}
if (isset($this->defaultResolver)) {
/** @var Resolver $resolver */
$resolver = app($this->defaultResolver);
return $resolver->resolve($url);
}
throw new \UnexpectedValueException('URL not matched.');
}
public function resolveWithAcceptHeader(string $url): ?Metadata
{
try {
// Rails等はAcceptに */* が入っていると、ブラウザの適当なAcceptヘッダだと判断して全部無視してしまう。
// c.f. https://github.com/rails/rails/issues/9940
// そこでここでは */* を「Acceptヘッダを無視してきたレスポンス(よくある)」のハンドラとして扱い、
// Acceptヘッダには */* を足さないことにする。
$acceptTypes = array_diff(array_keys($this->mimeTypes), ['*/*']);
$client = new \GuzzleHttp\Client();
$res = $client->request('GET', $url, [
'headers' => [
'Accept' => implode(', ', $acceptTypes)
]
]);
if ($res->getStatusCode() === 200) {
preg_match('/^[^;\s]+/', $res->getHeaderLine('Content-Type'), $matches);
$mimeType = $matches[0];
if (isset($this->mimeTypes[$mimeType])) {
$class = $this->mimeTypes[$mimeType];
$parser = new $class();
return $parser->parse($res->getBody());
}
if (isset($this->mimeTypes['*/*'])) {
$class = $this->mimeTypes['*/*'];
$parser = new $class();
return $parser->parse($res->getBody());
}
} else {
// code < 400 && code !== 200 => fallback
}
} catch (ClientException $e) {
// 406 Not Acceptable は多分Acceptが原因なので無視してフォールバック
if ($e->getResponse()->getStatusCode() !== 406) {
throw $e;
}
} catch (ServerException $e) {
// 5xx は変なAcceptが原因かもしれない(?)ので無視してフォールバック
}
return null;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class NarouResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$cookieJar = CookieJar::fromArray(['over18' => 'yes'], '.syosetu.com');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$metadata->description = '';
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
$xpath = new \DOMXPath($dom);
$description = [];
// 作者名
$writerNodes = $xpath->query('//*[contains(@class, "novel_writername")]');
if ($writerNodes->length !== 0 && !empty($writerNodes->item(0)->textContent)) {
$description[] = trim($writerNodes->item(0)->textContent);
}
// あらすじ
$exNodes = $xpath->query('//*[@id="novel_ex"]');
if ($exNodes->length !== 0 && !empty($exNodes->item(0)->textContent)) {
$summary = trim($exNodes->item(0)->textContent);
$description[] = mb_strimwidth($summary, 0, 101, '…'); // 100 + '…'(1)
}
$metadata->description = implode(' / ', $description);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class NicoSeigaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
// ページURLからサムネイルURLに変換
preg_match('~http://(?:(?:sp\\.)?seiga\\.nicovideo\\.jp/seiga(?:/#!)?|nico\\.ms)/im(\\d+)~', $url, $matches);
$metadata->image = "http://lohas.nicoseiga.jp/thumb/${matches[1]}l?";
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class NijieResolver implements Resolver
{
/**
* @var Client
*/
protected $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
if (mb_strpos($url, '//sp.nijie.info') !== false) {
$url = preg_replace('~//sp\.nijie\.info~', '//nijie.info', $url);
}
if (mb_strpos($url, 'view_popup.php') !== false) {
$url = preg_replace('~view_popup\.php~', 'view.php', $url);
}
$client = $this->client;
$res = $client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$dataNode = $xpath->query('//script[substring(@type, string-length(@type) - 3, 4) = "json"]');
foreach ($dataNode as $node) {
// 改行がそのまま入っていることがあるのでデコード前にエスケープが必要
$imageData = json_decode(preg_replace('/\r?\n/', '\n', $node->nodeValue), true);
if (isset($imageData['thumbnailUrl']) && !ends_with($imageData['thumbnailUrl'], '.gif') && !ends_with($imageData['thumbnailUrl'], '.mp4')) {
$metadata->image = preg_replace('~nijie\\.info/.*/nijie_picture/~', 'nijie.info/nijie_picture/', $imageData['thumbnailUrl']);
break;
}
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class OGPResolver implements Resolver, Parser
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
return $this->parse($res->getBody());
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
public function parse(string $html): Metadata
{
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
$xpath = new \DOMXPath($dom);
$metadata = new Metadata();
$metadata->title = $this->findContent($xpath, '//meta[@*="og:title"]', '//meta[@*="twitter:title"]');
if (empty($metadata->title)) {
$nodes = $xpath->query('//title');
if ($nodes->length !== 0) {
$metadata->title = $nodes->item(0)->textContent;
}
}
$metadata->description = $this->findContent($xpath, '//meta[@*="og:description"]', '//meta[@*="twitter:description"]', '//meta[@name="description"]');
$metadata->image = $this->findContent($xpath, '//meta[@*="og:image"]', '//meta[@*="twitter:image"]');
return $metadata;
}
private function findContent(\DOMXPath $xpath, string ...$expressions)
{
foreach ($expressions as $expression) {
$nodes = $xpath->query($expression);
foreach ($nodes as $node) {
$content = $node->getAttribute('content');
if (!empty($content)) {
return $content;
}
}
}
return '';
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\MetadataResolver;
interface Parser
{
public function parse(string $body): Metadata;
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\MetadataResolver;
use Carbon\Carbon;
use GuzzleHttp\Client;
class PatreonResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $temp);
$expires_at_unixtime = $temp['token-time'];
$expires_at = Carbon::createFromTimestamp($expires_at_unixtime);
$metadata->expires_at = $expires_at;
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class PixivResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
/**
* サムネイル画像 URL から最大長辺 1200px の画像 URL に変換する
*
* @param string $thumbnailUrl サムネイル画像 URL
*
* @return string 1200px の画像 URL
*/
public function thumbnailToMasterUrl(string $thumbnailUrl): string
{
$temp = str_replace('/c/128x128', '', $thumbnailUrl);
$largeUrl = str_replace('square1200.jpg', 'master1200.jpg', $temp);
return $largeUrl;
}
/**
* 直リン可能な pixiv.cat のプロキシ URL に変換する
* HUGE THANKS TO PIXIV.CAT!
*
* @param string $pixivUrl i.pximg URL
*
* @return string i.pixiv.cat URL
*/
public function proxize(string $pixivUrl): string
{
return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl);
}
public function resolve(string $url): Metadata
{
parse_str(parse_url($url, PHP_URL_QUERY), $params);
$illustId = $params['illust_id'];
$page = 0;
// 漫画ページ(ページ数はmanga_bigならあるかも)
if ($params['mode'] === 'manga_big' || $params['mode'] === 'manga') {
$page = $params['page'] ?? 0;
// 未ログインでは漫画ページを開けないため、URL を作品ページに変換する
$url = preg_replace('~mode=manga(_big)?~', 'mode=medium', $url);
}
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
preg_match("~https://i\.pximg\.net/c/128x128/img-master/img/\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}/{$illustId}(_p0)?_square1200\.jpg~", $res->getBody(), $match);
$illustThumbnailUrl = $match[0];
if ($page != 0) {
$illustThumbnailUrl = str_replace('_p0', '_p'.$page, $illustThumbnailUrl);
}
$illustUrl = $this->thumbnailToMasterUrl($illustThumbnailUrl);
$metadata->image = $this->proxize($illustUrl);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\MetadataResolver;
interface Resolver
{
public function resolve(string $url): Metadata;
}
@@ -0,0 +1,37 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class ToranoanaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$cookieJar = CookieJar::fromArray(['adflg' => '0'], 'ec.toranoana.jp');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) {
return $this->ogpResolver->parse($res->getBody());
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}
+12 -2
View File
@@ -2,7 +2,10 @@
namespace App\Providers; namespace App\Providers;
use App\MetadataResolver\MetadataResolver;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Parsedown;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -13,7 +16,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
// Blade::directive('parsedown', function ($expression) {
return "<?php echo app('parsedown')->text($expression); ?>";
});
} }
/** /**
@@ -23,6 +28,11 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register() public function register()
{ {
// $this->app->singleton(MetadataResolver::class, function ($app) {
return new MetadataResolver();
});
$this->app->singleton('parsedown', function () {
return Parsedown::instance();
});
} }
} }
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
{ {
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider class BroadcastServiceProvider extends ServiceProvider
{ {
+4 -4
View File
@@ -2,8 +2,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider class EventServiceProvider extends ServiceProvider
{ {
@@ -13,9 +13,9 @@ class EventServiceProvider extends ServiceProvider
* @var array * @var array
*/ */
protected $listen = [ protected $listen = [
'App\Events\Event' => [ 'App\Events\LinkDiscovered' => [
'App\Listeners\EventListener', 'App\Listeners\LinkCollector'
], ]
]; ];
/** /**
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@@ -2,7 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Http\ViewComposers\ProfileComposer; use App\Http\ViewComposers\ProfileStatsComposer;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@@ -15,7 +15,7 @@ class ViewComposerServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
View::composer('components.profile', ProfileComposer::class); View::composer('components.profile-stats', ProfileStatsComposer::class);
} }
/** /**
+2 -1
View File
@@ -2,8 +2,8 @@
namespace App; namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class User extends Authenticatable class User extends Authenticatable
@@ -39,6 +39,7 @@ class User extends Authenticatable
public function getProfileImageUrl($size = 30): string public function getProfileImageUrl($size = 30): string
{ {
$hash = md5(strtolower(trim($this->email))); $hash = md5(strtolower(trim($this->email)));
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size; return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size;
} }
+39
View File
@@ -24,6 +24,7 @@ class Formatter
$days = floor($value / 86400); $days = floor($value / 86400);
$hours = floor($value % 86400 / 3600); $hours = floor($value % 86400 / 3600);
$minutes = floor($value % 3600 / 60); $minutes = floor($value % 3600 / 60);
return "{$days}{$hours}時間 {$minutes}"; return "{$days}{$hours}時間 {$minutes}";
} }
@@ -36,4 +37,42 @@ class Formatter
{ {
return $this->linkify->processUrls($text); return $this->linkify->processUrls($text);
} }
/**
* URLを正規化します。
* @param string $url URL
* @return string 正規化されたURL
*/
public function normalizeUrl($url)
{
// Decode
$url = urldecode($url);
// Remove Hashbang
$url = preg_replace('~/#!/~u', '/', $url);
// Sort query parameters
$parts = parse_url($url);
if (!empty($parts['query'])) {
// Remove query parameters
$url = str_replace_last('?' . $parts['query'], '', $url);
if (!empty($parts['fragment'])) {
// Remove fragment identifier
$url = str_replace_last('#' . $parts['fragment'], '', $url);
} else {
// "http://example.com/?query#" の場合 $parts['fragment'] は unset になるので、個別に判定して除去する必要がある
$url = preg_replace('/#\z/u', '', $url);
}
parse_str($parts['query'], $params);
ksort($params);
$url = $url . '?' . http_build_query($params);
if (!empty($parts['fragment'])) {
$url .= '#' . $parts['fragment'];
}
}
return $url;
}
} }
+12 -4
View File
@@ -5,19 +5,24 @@
"license": "MIT", "license": "MIT",
"type": "project", "type": "project",
"require": { "require": {
"php": ">=7.0.0", "php": ">=7.1.0",
"anhskohbo/no-captcha": "^3.0",
"doctrine/dbal": "^2.9",
"fideloper/proxy": "~3.3",
"guzzlehttp/guzzle": "^6.3", "guzzlehttp/guzzle": "^6.3",
"laravel/framework": "5.5.*", "laravel/framework": "5.5.*",
"laravel/tinker": "~1.0", "laravel/tinker": "~1.0",
"misd/linkify": "^1.1", "misd/linkify": "^1.1"
"parsedown/laravel": "~1.0"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.1", "barryvdh/laravel-debugbar": "^3.1",
"barryvdh/laravel-ide-helper": "^2.5",
"filp/whoops": "~2.0", "filp/whoops": "~2.0",
"friendsofphp/php-cs-fixer": "^2.14",
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",
"mockery/mockery": "~1.0", "mockery/mockery": "~1.0",
"phpunit/phpunit": "~6.0" "phpunit/phpunit": "~6.0",
"symfony/thanks": "^1.0"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [
@@ -42,6 +47,9 @@
"post-autoload-dump": [ "post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover" "@php artisan package:discover"
],
"fix": [
"php-cs-fixer fix"
] ]
}, },
"config": { "config": {
Generated
+2105 -498
View File
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration class CreateUsersTable extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePasswordResetsTable extends Migration class CreatePasswordResetsTable extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateEjaculationsTable extends Migration class CreateEjaculationsTable extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInformationTable extends Migration class CreateInformationTable extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLinkToEjaculations extends Migration class AddLinkToEjaculations extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTagsTable extends Migration class CreateTagsTable extends Migration
{ {
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMetadataTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('metadata', function (Blueprint $table) {
$table->string('url');
$table->string('title');
$table->string('description');
$table->string('image');
$table->timestamps();
$table->index('url');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('metadata');
}
}
@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RecreateMetadataTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('metadata');
Schema::create('metadata', function (Blueprint $table) {
$table->text('url');
$table->text('title');
$table->text('description');
$table->text('image');
$table->timestamps();
$table->index('url');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('metadata');
}
}
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeLinkOnEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->text('link')->default('')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->string('link')->default('')->change();
});
}
}
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddExpiresOnMetadata extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('metadata', function (Blueprint $table) {
$table->timestamp('expires_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('metadata', function (Blueprint $table) {
$table->removeColumn('expires_at');
});
}
}
@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBioAndUrlToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('bio', 160)->default('');
$table->text('url')->default('');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('bio');
$table->dropColumn('url');
});
}
}
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -e
if [[ "$APP_DEBUG" == "true" ]]; then
export PHP_INI_SCAN_DIR=":/usr/local/etc/php/php.d"
fi
exec docker-php-entrypoint "$@"
+4
View File
@@ -0,0 +1,4 @@
; Dockerでのデバッグ用設定
zend_extension=xdebug.so
xdebug.remote_enable=true
xdebug.remote_host=host.docker.internal
+6
View File
@@ -0,0 +1,6 @@
version: "3"
services:
db:
ports:
- 5432:5432
+33
View File
@@ -0,0 +1,33 @@
version: "3"
services:
web:
build: .
env_file:
- .env
volumes:
- .:/var/www/html
networks:
- backend
ports:
- 4545:80
restart: always
depends_on:
- db
db:
image: postgres:10-alpine
environment:
POSTGRES_DB: tissue
POSTGRES_USER: tissue
POSTGRES_PASSWORD: tissue
volumes:
- db:/var/lib/postgresql/data
networks:
- backend
restart: always
networks:
backend:
volumes:
db:
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# https://laravel.com/docs/5.5/deployment
composer install --optimize-autoloader
php artisan config:cache
-1353
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+6 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-330
View File
@@ -1,330 +0,0 @@
html {
box-sizing: border-box;
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1rem;
font-weight: normal;
line-height: 1.5;
color: #212529;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: bold;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
a,
area,
button,
[role="button"],
input,
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #868e96;
text-align: left;
caption-side: bottom;
}
th {
text-align: left;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -1,2 +1,8 @@
html{box-sizing:border-box;font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}*,::after,::before{box-sizing:inherit}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important} /*!
* Bootstrap Reboot v4.1.1 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */ /*# sourceMappingURL=bootstrap-reboot.min.css.map */
File diff suppressed because one or more lines are too long
-8185
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+4 -4
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+39
View File
@@ -15,6 +15,25 @@
overflow-x: auto; overflow-x: auto;
} }
.tis-need-agecheck .container {
filter: blur(45px);
pointer-events: none;
}
.container {
transition: filter .15s liner;
}
.list-group-item.no-side-border {
border-left: none;
border-right: none;
border-radius: 0;
}
.list-group-item.border-bottom-only:first-child {
border-top: none;
}
.list-group-item.border-bottom-only { .list-group-item.border-bottom-only {
border-left: none; border-left: none;
border-right: none; border-right: none;
@@ -28,3 +47,23 @@
.timeline-action-item { .timeline-action-item {
margin-left: 16px; margin-left: 16px;
} }
.tis-global-count-graph {
height: 90px;
border-bottom: 1px solid rgba(0, 0, 0, .125);
}
.tis-page-selector {
margin-left: -1px;
width: calc(100% + 2px);
height: 100%;
border: 1px solid #dee2e6;
border-radius: 0;
line-height: 1.25;
}
@media (min-width: 992px) {
.tis-sidebar-info {
font-size: small;
}
}
+1742 -1646
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
File diff suppressed because one or more lines are too long
+55
View File
@@ -0,0 +1,55 @@
// app.jsの名はモジュールバンドラーを投入する日まで予約しておく。CSSも同じ。
(function ($) {
$.fn.linkCard = function (options) {
var settings = $.extend({
endpoint: '/api/checkin/card'
}, options);
return this.each(function () {
var $this = $(this);
$.ajax({
url: settings.endpoint,
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $this.find('img');
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} else {
$desc.text(data.description);
}
if (data.image === '') {
$image.hide();
} else {
$image.attr('src', data.image);
}
if (data.title !== '' || data.description !== '' || data.image !== '') {
$this.removeClass('d-none');
}
});
});
};
$.fn.pageSelector = function () {
return this.on('change', function () {
location.href = $(this).find(':selected').data('href');
});
};
})(jQuery);
+4
View File
@@ -99,6 +99,10 @@ return [
'attribute-name' => [ 'attribute-name' => [
'rule-name' => 'custom-message', 'rule-name' => 'custom-message',
], ],
'g-recaptcha-response' => [
'required' => '「私はロボットではありません」にチェックを入れてください。',
'captcha' => 'reCAPTCHAチェックに失敗しました。何度試しても解決しない場合、管理者にお問い合わせください。',
],
], ],
/* /*
+6 -7
View File
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', 'ログイン')
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>ログイン</h2> <h2>ログイン</h2>
@@ -11,7 +13,7 @@
<div class="form-group"> <div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label> <label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required autofocus> <input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email')) @if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div> <div class="invalid-feedback">{{ $errors->first('email') }}</div>
@@ -25,12 +27,9 @@
<div class="invalid-feedback">{{ $errors->first('password') }}</div> <div class="invalid-feedback">{{ $errors->first('password') }}</div>
@endif @endif
</div> </div>
<div class="form-check"> <div class="custom-control custom-checkbox mb-3">
<label class="custom-control custom-checkbox"> <input id="remember" name="remember" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
<input id="remember" name="rememver" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}> <label class="custom-control-label" for="remember">保存する</label>
<span class="custom-control-indicator"></span>
<span class="custom-control-description">保存する</span>
</label>
</div> </div>
<button class="btn btn-primary" type="submit">ログイン</button> <button class="btn btn-primary" type="submit">ログイン</button>
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', 'パスワードの再発行')
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>パスワードの再発行</h2> <h2>パスワードの再発行</h2>
@@ -14,7 +16,7 @@
<div class="form-group"> <div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label> <label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required autofocus> <input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email')) @if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div> <div class="invalid-feedback">{{ $errors->first('email') }}</div>
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', 'パスワードの再発行')
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>パスワードの再発行</h2> <h2>パスワードの再発行</h2>
@@ -16,7 +18,7 @@
<div class="form-group"> <div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label> <label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required> <input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required>
@if ($errors->has('email')) @if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div> <div class="invalid-feedback">{{ $errors->first('email') }}</div>
+27 -11
View File
@@ -1,9 +1,21 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', '新規登録')
@push('head')
@if (!empty(config('captcha.secret')))
{!! NoCaptcha::renderJs() !!}
@endif
@endpush
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>新規登録</h2> <h2>新規登録</h2>
<hr> <hr>
<div class="alert alert-warning">
<p class="mb-0"><strong>注意!</strong> Tissueでは、登録に使用したメールアドレスの <a href="https://ja.gravatar.com/" rel="noreferrer">Gravatar</a> を使用します。</p>
<p class="mb-0">他の場所での活動と紐付いてほしくない場合、使用予定のメールアドレスにGravatarが設定されていないかを確認することを推奨します。</p>
</div>
<div class="row justify-content-center my-5"> <div class="row justify-content-center my-5">
<div class="col-lg-6"> <div class="col-lg-6">
<form method="post" action="{{ route('register') }}"> <form method="post" action="{{ route('register') }}">
@@ -19,7 +31,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label> <label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required> <input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required>
@if ($errors->has('email')) @if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div> <div class="invalid-feedback">{{ $errors->first('email') }}</div>
@@ -41,22 +53,26 @@
<h6 class="mb-3">プライバシーに関するオプション (全て任意です)</h6> <h6 class="mb-3">プライバシーに関するオプション (全て任意です)</h6>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="custom-control custom-checkbox mb-2">
<label class="custom-control custom-checkbox">
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}> <input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
<span class="custom-control-indicator"></span> <label class="custom-control-label" for="protected">全てのチェックイン履歴を非公開にする</label>
<span class="custom-control-description">全てのチェックイン履歴を非公開にする</span>
</label>
</div> </div>
<div class="form-check"> <div class="custom-control custom-checkbox">
<label class="custom-control custom-checkbox">
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}> <input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
<span class="custom-control-indicator"></span> <label class="custom-control-label" for="accept-analytics">匿名での統計にチェックインデータを利用することに同意します</label>
<span class="custom-control-description">匿名での統計にチェックインデータを利用することに同意します</span>
</label>
</div> </div>
</div> </div>
</div> </div>
@if (!empty(config('captcha.secret')))
<div class="form-row ml-1 mt-2 my-4">
<div class="mx-auto">
{!! NoCaptcha::display() !!}
</div>
@if ($errors->has('g-recaptcha-response'))
<div class="invalid-feedback d-block text-center">{{ $errors->first('g-recaptcha-response') }}</div>
@endif
</div>
@endif
<div class="text-center"> <div class="text-center">
<button class="btn btn-primary btn-lg" type="submit">登録</button> <button class="btn btn-primary btn-lg" type="submit">登録</button>
@@ -0,0 +1,9 @@
<div class="card link-card mb-2 px-0 col-12 col-md-6 d-none" style="font-size: small;">
<a class="text-dark card-link" href="{{ $link }}" target="_blank" rel="noopener">
<img src="" alt="Thumbnail" class="card-img-top bg-secondary">
<div class="card-body">
<h6 class="card-title font-weight-bold">タイトル</h6>
<p class="card-text">コンテンツの説明文</p>
</div>
</a>
</div>
@@ -0,0 +1,15 @@
<h6 class="font-weight-bold"><span class="oi oi-timer"></span> 現在のセッション</h6>
@if (isset($currentSession))
<p class="card-text mb-0">{{ $currentSession }}経過</p>
<p class="card-text">({{ $latestEjaculation->ejaculated_date->format('Y/m/d H:i') }} にリセット)</p>
@else
<p class="card-text mb-0">計測がまだ始まっていません</p>
<p class="card-text">(一度チェックインすると始まります)</p>
@endif
<h6 class="font-weight-bold"><span class="oi oi-graph"></span> 概況</h6>
<p class="card-text mb-0">平均記録: {{ Formatter::formatInterval($summary[0]->average) }}</p>
<p class="card-text mb-0">最長記録: {{ Formatter::formatInterval($summary[0]->longest) }}</p>
<p class="card-text mb-0">最短記録: {{ Formatter::formatInterval($summary[0]->shortest) }}</p>
<p class="card-text mb-0">合計時間: {{ Formatter::formatInterval($summary[0]->total_times) }}</p>
<p class="card-text">通算回数: {{ $summary[0]->total_checkins }}</p>
+21 -15
View File
@@ -1,6 +1,6 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<img src="{{ $user->getProfileImageUrl(64) }}" class="rounded mb-1"> <img src="{{ $user->getProfileImageUrl(128) }}" class="rounded mb-1">
<h4 class="card-title"> <h4 class="card-title">
<a class="text-dark" href="{{ route('user.profile', ['name' => $user->name]) }}">{{ $user->display_name }}</a> <a class="text-dark" href="{{ route('user.profile', ['name' => $user->name]) }}">{{ $user->display_name }}</a>
</h4> </h4>
@@ -11,22 +11,28 @@
@endif @endif
</h6> </h6>
@if (!$user->is_protected) {{-- Bio --}}
<h6 class="font-weight-bold mt-4"><span class="oi oi-timer"></span> 現在のセッション</h6> @if (!empty($user->bio))
@if (isset($currentSession)) <p class="card-text mt-3 mb-0">
<p class="card-text mb-0">{{ $currentSession }}経過</p> {!! Formatter::linkify(nl2br(e($user->bio))) !!}
<p class="card-text">({{ $latestEjaculation->ejaculated_date->format('Y/m/d H:i') }} にリセット)</p> </p>
@else
<p class="card-text mb-0">計測がまだ始まっていません</p>
<p class="card-text">(一度チェックインすると始まります)</p>
@endif @endif
<h6 class="font-weight-bold"><span class="oi oi-graph"></span> 概況</h6> {{-- URL --}}
<p class="card-text mb-0">平均記録: {{ Formatter::formatInterval($summary[0]->average) }}</p> @if (!empty($user->url))
<p class="card-text mb-0">最長記録: {{ Formatter::formatInterval($summary[0]->longest) }}</p> <p class="card-text d-flex mt-3">
<p class="card-text mb-0">最短記録: {{ Formatter::formatInterval($summary[0]->shortest) }}</p> <span class="oi oi-link-intact mr-1 mt-1"></span>
<p class="card-text mb-0">合計時間: {{ Formatter::formatInterval($summary[0]->total_times) }}</p> <a href="{{ $user->url }}" rel="me nofollow noopener" target="_blank" class="text-truncate">{{ preg_replace('~\Ahttps?://~', '', $user->url) }}</a>
<p class="card-text">通算回数: {{ $summary[0]->total_checkins }}</p> </p>
@endif @endif
</div> </div>
</div> </div>
@if (!$user->is_protected || $user->isMe())
<div class="card mb-4">
<div class="card-body">
@component('components.profile-stats', ['user' => $user])
@endcomponent
</div>
</div>
@endif
+69 -12
View File
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', 'チェックイン')
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>今致してる?</h2> <h2>今致してる?</h2>
@@ -13,7 +15,7 @@
<div class="form-group col-sm-6"> <div class="form-group col-sm-6">
<label for="date"><span class="oi oi-calendar"></span> 日付</label> <label for="date"><span class="oi oi-calendar"></span> 日付</label>
<input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}" <input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}"
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? date('Y/m/d') }}" required> pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? $defaults['date'] }}" required>
@if ($errors->has('date')) @if ($errors->has('date'))
<div class="invalid-feedback">{{ $errors->first('date') }}</div> <div class="invalid-feedback">{{ $errors->first('date') }}</div>
@@ -22,7 +24,7 @@
<div class="form-group col-sm-6"> <div class="form-group col-sm-6">
<label for="time"><span class="oi oi-clock"></span> 時刻</label> <label for="time"><span class="oi oi-clock"></span> 時刻</label>
<input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}" <input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}"
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? date('H:i') }}" required> pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? $defaults['time'] }}" required>
@if ($errors->has('time')) @if ($errors->has('time'))
<div class="invalid-feedback">{{ $errors->first('time') }}</div> <div class="invalid-feedback">{{ $errors->first('time') }}</div>
@@ -36,9 +38,9 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<input name="tags" type="hidden" value="{{ old('tags') }}"> <input name="tags" type="hidden" value="{{ old('tags') ?? $defaults['tags'] }}">
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label> <label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
<div class="form-control {{ $errors->has('tags') ? ' is-invalid' : '' }}"> <div class="form-control h-auto {{ $errors->has('tags') ? ' is-invalid' : '' }}">
<ul id="tags" class="list-inline d-inline"></ul> <ul id="tags" class="list-inline d-inline"></ul>
<input id="tagInput" type="text" style="outline: 0; border: 0;"> <input id="tagInput" type="text" style="outline: 0; border: 0;">
</div> </div>
@@ -54,7 +56,7 @@
<div class="form-row"> <div class="form-row">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label> <label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
<input id="link" name="link" type="text" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') }}"> <input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $defaults['link'] }}">
<small class="form-text text-muted"> <small class="form-text text-muted">
オカズのURLを貼り付けて登録することができます。 オカズのURLを貼り付けて登録することができます。
</small> </small>
@@ -62,11 +64,13 @@
<div class="invalid-feedback">{{ $errors->first('link') }}</div> <div class="invalid-feedback">{{ $errors->first('link') }}</div>
@endif @endif
</div> </div>
@component('components.card', ['link' => null])
@endcomponent
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<label for="note"><span class="oi oi-comment-square"></span> ノート</label> <label for="note"><span class="oi oi-comment-square"></span> ノート</label>
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') }}</textarea> <textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $defaults['note'] }}</textarea>
<small class="form-text text-muted"> <small class="form-text text-muted">
最大 500 文字 最大 500 文字
</small> </small>
@@ -78,13 +82,10 @@
<div class="form-row mt-4"> <div class="form-row mt-4">
<p>オプション</p> <p>オプション</p>
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<div class="form-check"> <div class="custom-control custom-checkbox mb-3">
<label class="custom-control custom-checkbox"> <input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') || $defaults['is_private'] ? 'checked' : '' }}>
<input name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') ? 'checked' : '' }}> <label class="custom-control-label" for="isPrivate">
<span class="custom-control-indicator"></span>
<span class="custom-control-description">
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする <span class="oi oi-lock-locked"></span> このチェックインを非公開にする
</span>
</label> </label>
</div> </div>
</div> </div>
@@ -150,4 +151,60 @@
$('#tagInput').focus(); $('#tagInput').focus();
}); });
</script> </script>
<script>
document.addEventListener(
"DOMContentLoaded",
function() { document.getElementById("link").onchange = linkChanged;},
false
);
function linkChanged(event) {
const card = document.querySelector(".link-card");
const url = event.target.value;
hiddenCard(card);
updateCard(card, url);
return null;
}
async function updateCard(card, url) {
const data = await getCardInfo(url);
card.querySelector(".card-title").innerHTML = data.title;
card.querySelector(".card-text").innerHTML = data.description;
if(data.image){
card.querySelector("img").src = data.image;
}else {
card.querySelector("img").classList.add("d-none");
}
card.classList.remove('col-md-6');
showCard(card);
return null;
}
function getCardInfo(url) {
return $.ajax({
url: '/api/checkin/card',
method: 'get',
type: 'json',
data: {
url: url
}
}).then(function (data) {
console.log(data);
return data;
}).catch(function(e) {
console.log(e); // "oh, no!"
return {
title: "Error",
description: "Error",
image: null
};
});
}
function showCard(card) {
card.classList.remove("d-none");
return null;
}
function hiddenCard(card) {
card.classList.add("d-none");
return null;
}
</script>
@endpush @endpush
+8 -20
View File
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', 'チェックインの修正')
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>チェックインの修正</h2> <h2>チェックインの修正</h2>
@@ -37,9 +39,9 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<input name="tags" type="hidden" value="{{ old('tags') ?? implode(' ', $ejaculation->tags->map(function ($v) { return $v->name; })->all()) }}"> <input name="tags" type="hidden" value="{{ old('tags') ?? $ejaculation->textTags() }}">
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label> <label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
<div class="form-control {{ $errors->has('tags') ? ' is-invalid' : '' }}"> <div class="form-control h-auto {{ $errors->has('tags') ? ' is-invalid' : '' }}">
<ul id="tags" class="list-inline d-inline"></ul> <ul id="tags" class="list-inline d-inline"></ul>
<input id="tagInput" type="text" style="outline: 0; border: 0;"> <input id="tagInput" type="text" style="outline: 0; border: 0;">
</div> </div>
@@ -52,21 +54,10 @@
@endif @endif
</div> </div>
</div> </div>
{{--
<div class="form-row">
<div class="form-group col-sm-12">
<label for="tags"><span class="oi oi-tags"></span> タグ</label>
<input id="tags" type="text" class="form-control" placeholder="未実装です" disabled>
<small class="form-text text-muted">
スペース区切りで複数入力できます。
</small>
</div>
</div>
--}}
<div class="form-row"> <div class="form-row">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label> <label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
<input id="link" name="link" type="text" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}"> <input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}">
<small class="form-text text-muted"> <small class="form-text text-muted">
オカズのURLを貼り付けて登録することができます。 オカズのURLを貼り付けて登録することができます。
</small> </small>
@@ -90,13 +81,10 @@
<div class="form-row mt-4"> <div class="form-row mt-4">
<p>オプション</p> <p>オプション</p>
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<div class="form-check"> <div class="custom-control custom-checkbox mb-3">
<label class="custom-control custom-checkbox"> <input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
<input name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}> <label class="custom-control-label" for="isPrivate">
<span class="custom-control-indicator"></span>
<span class="custom-control-description">
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする <span class="oi oi-lock-locked"></span> このチェックインを非公開にする
</span>
</label> </label>
</div> </div>
</div> </div>
+18 -50
View File
@@ -1,5 +1,11 @@
@extends('layouts.base') @extends('layouts.base')
@if (!$user->isMe() && ($user->is_protected || $ejaculation->is_private))
@section('title', $user->display_name . ' さんのチェックイン')
@else
@section('title', $user->display_name . ' さんのチェックイン (' . $ejaculation->ejaculated_date->format('n月j日 H:i') . ')')
@endif
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="row"> <div class="row">
@@ -26,13 +32,14 @@
<!-- span --> <!-- span -->
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5> <h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
@if ($user->isMe())
<div> <div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
@if ($user->isMe())
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a> <a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a> <a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
</div>
@endif @endif
</div> </div>
</div>
<!-- tags --> <!-- tags -->
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty()) @if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
<p class="mb-2"> <p class="mb-2">
@@ -40,24 +47,19 @@
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span> <span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
@endif @endif
@foreach ($ejaculation->tags as $tag) @foreach ($ejaculation->tags as $tag)
<span class="badge badge-secondary"><span class="oi oi-tag"></span> {{ $tag->name }}</span> <a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach @endforeach
</p> </p>
@endif @endif
<!-- okazu link --> <!-- okazu link -->
@if (!empty($ejaculation->link)) @if (!empty($ejaculation->link))
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;"> <div class="row mx-0">
<a class="text-dark card-link" href="{{ $ejaculation->link }}" target="_blank" rel="noopener"> @component('components.card', ['link' => $ejaculation->link])
<img src="" alt="Thumbnail" class="card-img-top bg-secondary"> @endcomponent
<div class="card-body"> <p class="d-flex align-items-baseline mb-2 col-12 px-0">
<h6 class="card-title font-weight-bold">タイトル</h6> <span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
<p class="card-text">コンテンツの説明文</p>
</div>
</a>
</div>
<p class="mb-2">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p> </p>
</div>
@endif @endif
<!-- note --> <!-- note -->
@if (!empty($ejaculation->note)) @if (!empty($ejaculation->note))
@@ -103,42 +105,8 @@
form.submit(); form.submit();
}); });
$('.link-card').each(function () { $('.link-card').linkCard({
var $this = $(this); endpoint: '{{ url('/api/checkin/card') }}'
$.ajax({
url: '{{ url('/api/checkin/card') }}',
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $this.find('img');
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} else {
$desc.text(data.description);
}
if (data.image === '') {
$image.hide();
} else {
$image.attr('src', data.image);
}
if (data.title !== '' || data.description !== '' || data.image !== '') {
$this.removeClass('d-none');
}
});
}); });
</script> </script>
@endpush @endpush
+3
View File
@@ -31,6 +31,9 @@
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
@foreach($informations as $info) @foreach($informations as $info)
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}"> <a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small> <span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
</a> </a>
@endforeach @endforeach
+80 -59
View File
@@ -7,59 +7,81 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-lg-4"> <div class="col-lg-4">
@component('components.profile', ['user' => Auth::user()]) <div class="card mb-4">
<div class="card-body">
<div class="d-flex flex-row align-items-end mb-4">
<img src="{{ Auth::user()->getProfileImageUrl(48) }}" class="rounded mr-2">
<div class="d-flex flex-column overflow-hidden">
<h5 class="card-title text-truncate">
<a class="text-dark" href="{{ route('user.profile', ['name' => Auth::user()->name]) }}">{{ Auth::user()->display_name }}</a>
</h5>
<h6 class="card-subtitle">
<a class="text-muted" href="{{ route('user.profile', ['name' => Auth::user()->name]) }}">&commat;{{ Auth::user()->name }}</a>
@if (Auth::user()->is_protected)
<span class="oi oi-lock-locked text-muted"></span>
@endif
</h6>
</div>
</div>
@component('components.profile-stats', ['user' => Auth::user()])
@endcomponent @endcomponent
</div> </div>
<div class="col-lg-8"> </div>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header">サイトからのお知らせ</div> <div class="card-header">サイトからのお知らせ</div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush tis-sidebar-info">
@foreach($informations as $info) @foreach($informations as $info)
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}"> <a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small> <span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
</a> </a>
@endforeach @endforeach
<a href="{{ route('info') }}" class="list-group-item text-right">お知らせ一覧 &raquo;</a> <a href="{{ route('info') }}" class="list-group-item text-right">お知らせ一覧 &raquo;</a>
</div> </div>
</div> </div>
@if (!empty($publicLinkedEjaculations))
<div class="card mb-4">
<div class="card-header">お惣菜コーナー</div>
<div class="card-body">
<p class="card-text">最近の公開チェックインから、オカズリンク付きのものを表示しています。</p>
</div> </div>
<ul class="list-group list-group-flush"> <div class="col-lg-8">
@if (!empty($globalEjaculationCounts))
<h5>チェックインの動向</h5>
<div class="w-100 mb-4 position-relative tis-global-count-graph">
<canvas id="global-count-graph"></canvas>
</div>
@endif
@if (!empty($publicLinkedEjaculations))
<h5 class="mb-3">お惣菜コーナー</h5>
<p class="text-secondary">最近の公開チェックインから、オカズリンク付きのものを表示しています。</p>
<ul class="list-group">
@foreach ($publicLinkedEjaculations as $ejaculation) @foreach ($publicLinkedEjaculations as $ejaculation)
<li class="list-group-item pt-3 pb-3"> <li class="list-group-item no-side-border pt-3 pb-3 tis-word-wrap">
<!-- span --> <!-- span -->
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5> <h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user_id]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> &commat;{{ $ejaculation->user->name }}</a> <a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> {{ $ejaculation->user->display_name }}</a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a> <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5> </h5>
<div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
</div>
</div> </div>
<!-- tags --> <!-- tags -->
@if ($ejaculation->tags->isNotEmpty()) @if ($ejaculation->tags->isNotEmpty())
<p class="mb-2"> <p class="mb-2">
@foreach ($ejaculation->tags as $tag) @foreach ($ejaculation->tags as $tag)
<span class="badge badge-secondary"><span class="oi oi-tag"></span> {{ $tag->name }}</span> <a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach @endforeach
</p> </p>
@endif @endif
<!-- okazu link --> <!-- okazu link -->
@if (!empty($ejaculation->link)) @if (!empty($ejaculation->link))
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;"> <div class="row mx-0">
<a class="text-dark card-link" href="{{ $ejaculation->link }}" target="_blank" rel="noopener"> @component('components.card', ['link' => $ejaculation->link])
<img src="" alt="Thumbnail" class="card-img-top bg-secondary"> @endcomponent
<div class="card-body"> <p class="d-flex align-items-baseline mb-2 col-12 px-0">
<h6 class="card-title font-weight-bold">タイトル</h6> <span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
<p class="card-text">コンテンツの説明文</p>
</div>
</a>
</div>
<p class="mb-2">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p> </p>
</div>
@endif @endif
<!-- note --> <!-- note -->
@if (!empty($ejaculation->note)) @if (!empty($ejaculation->note))
@@ -70,7 +92,6 @@
</li> </li>
@endforeach @endforeach
</ul> </ul>
</div>
@endif @endif
</div> </div>
</div> </div>
@@ -78,43 +99,43 @@
@endsection @endsection
@push('script') @push('script')
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/[email protected]/dist/Chart.min.js"></script>
<script> <script>
$('.link-card').each(function () { $('.link-card').linkCard({
var $this = $(this); endpoint: '{{ url('/api/checkin/card') }}'
$.ajax({
url: '{{ url('/api/checkin/card') }}',
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $this.find('img');
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} else {
$desc.text(data.description);
}
if (data.image === '') {
$image.hide();
} else {
$image.attr('src', data.image);
}
if (data.title !== '' || data.description !== '' || data.image !== '') {
$this.removeClass('d-none');
}
}); });
new Chart(document.getElementById('global-count-graph').getContext('2d'), {
type: 'bar',
data: {
labels: @json(array_keys($globalEjaculationCounts)),
datasets: [{
data: @json(array_values($globalEjaculationCounts)),
backgroundColor: 'rgba(0, 0, 0, .1)',
borderColor: 'rgba(0, 0, 0, .25)',
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
legend: {
display: false
},
elements: {
line: {}
},
scales: {
xAxes: [{
display: false
}],
yAxes: [{
display: false,
ticks: {
beginAtZero: true
}
}]
}
}
}); });
</script> </script>
@endpush @endpush
+6 -17
View File
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', 'お知らせ')
@section('content') @section('content')
<div class="container"> <div class="container">
<h2>サイトからのお知らせ</h2> <h2>サイトからのお知らせ</h2>
@@ -7,26 +9,13 @@
<div class="list-group"> <div class="list-group">
@foreach($informations as $info) @foreach($informations as $info)
<a class="list-group-item border-bottom-only pt-3 pb-3" href="{{ route('info.show', ['id' => $info->id]) }}"> <a class="list-group-item border-bottom-only pt-3 pb-3" href="{{ route('info.show', ['id' => $info->id]) }}">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small> <span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
</a> </a>
@endforeach @endforeach
</div> </div>
<ul class="pagination mt-4 justify-content-center"> {{ $informations->links(null, ['className' => 'mt-4 justify-content-center']) }}
<li class="page-item {{ $informations->currentPage() === 1 ? 'disabled' : '' }}">
<a class="page-link" href="{{ $informations->previousPageUrl() }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
@for ($i = 1; $i <= $informations->lastPage(); $i++)
<li class="page-item {{ $i === $informations->currentPage() ? 'active' : '' }}"><a href="{{ $informations->url($i) }}" class="page-link">{{ $i }}</a></li>
@endfor
<li class="page-item {{ $informations->currentPage() === $informations->lastPage() ? 'disabled' : '' }}">
<a class="page-link" href="{{ $informations->nextPageUrl() }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</div> </div>
@endsection @endsection
+8 -1
View File
@@ -1,5 +1,7 @@
@extends('layouts.base') @extends('layouts.base')
@section('title', $category['label'] . ': ' . $info->title)
@section('content') @section('content')
<div class="container"> <div class="container">
<nav aria-label="breadcrumb" role="navigation"> <nav aria-label="breadcrumb" role="navigation">
@@ -9,7 +11,12 @@
</ol> </ol>
</nav> </nav>
<h2><span class="badge {{ $category['class'] }}">{{ $category['label'] }}</span> {{ $info->title }}</h2> <h2><span class="badge {{ $category['class'] }}">{{ $category['label'] }}</span> {{ $info->title }}</h2>
<p class="text-secondary"><span class="oi oi-calendar"></span> {{ $info->created_at->format('Y年n月j日') }}</p> <p class="text-secondary">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="oi oi-calendar"></span> {{ $info->created_at->format('Y年n月j日') }}
</p>
@parsedown($info->content) @parsedown($info->content)
</div> </div>
@endsection @endsection
+76 -11
View File
@@ -6,7 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
@hasSection('title')
<title>@yield('title') - {{ config('app.name', 'Tissue') }}</title>
@else
<title>{{ config('app.name', 'Tissue') }}</title> <title>{{ config('app.name', 'Tissue') }}</title>
@endif
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet"> <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ asset('css/open-iconic-bootstrap.min.css') }}" rel="stylesheet"> <link href="{{ asset('css/open-iconic-bootstrap.min.css') }}" rel="stylesheet">
@@ -14,7 +18,18 @@
@stack('head') @stack('head')
</head> </head>
<body> <body class="{{Auth::check() ? '' : 'tis-need-agecheck'}}">
<noscript class="navbar navbar-light bg-warning">
<div class="container-fluid">
<div class="d-flex flex-column mx-auto">
<p class="m-0 text-dark">Tissueを利用するには、ブラウザのJavaScriptとCookieを有効にする必要があります。</p>
<p class="m-0 text-info">
<a href="https://www.enable-javascript.com/ja/" target="_blank" rel="nofollow noopener">ブラウザでJavaScriptを有効にする方法</a>
<a href="https://www.whatismybrowser.com/guides/how-to-enable-cookies/auto" target="_blank" rel="nofollow noopener">ブラウザでCookieを有効にする方法</a>
</p>
</div>
</div>
</noscript>
<nav class="navbar navbar-expand-lg navbar-light bg-light {{ !Auth::check() && Route::currentRouteName() === 'home' ? '' : 'mb-4'}}"> <nav class="navbar navbar-expand-lg navbar-light bg-light {{ !Auth::check() && Route::currentRouteName() === 'home' ? '' : 'mb-4'}}">
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
{{ csrf_field() }} {{ csrf_field() }}
@@ -44,24 +59,43 @@
<a class="nav-link" href="{{ route('ranking') }}">ランキング</a> <a class="nav-link" href="{{ route('ranking') }}">ランキング</a>
</li>--}} </li>--}}
</ul> </ul>
<form action="{{ stripos(Route::currentRouteName(), 'search') === 0 ? route(Route::currentRouteName()) : route('search') }}" class="form-inline mr-2">
<div class="input-group">
<input type="search" name="q" class="form-control" placeholder="検索..." value="{{ stripos(Route::currentRouteName(), 'search') === 0 ? $inputs['q'] : '' }}" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit"><span class="oi oi-magnifying-glass" aria-hidden="true"></span><span class="sr-only">検索</span></button>
</div>
</div>
</form>
<form class="form-inline mr-2">
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
</form>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a href="#" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top mr-2"> <img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top">
{{ Auth::user()->display_name }} さん
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
{{--<a href="#" class="dropdown-item">設定</a>--}} <a href="{{ route('user.profile', ['name' => Auth::user()->name]) }}" class="dropdown-item">
<strong>{{ Auth::user()->display_name }}</strong>
<p class="mb-0 text-muted">
<span>&commat;{{ Auth::user()->name }}</span>
</p>
</a>
<div class="dropdown-divider"></div>
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a> <a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
</div> </div>
</li> </li>
</ul> </ul>
<form class="form-inline">
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
</form>
@endauth @endauth
@guest @guest
<form class="form-inline ml-auto"> <ul class="navbar-nav ml-auto mr-2">
<li class="nav-item">
<a href="{{ route('register') }}" class="nav-link">会員登録</a>
</li>
</ul>
<form class="form-inline">
<a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a> <a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a>
</form> </form>
@endguest @endguest
@@ -81,21 +115,52 @@
@yield('content') @yield('content')
<footer class="tis-footer mt-4"> <footer class="tis-footer mt-4">
<div class="container p-3 p-md-4"> <div class="container p-3 p-md-4">
<p>Copyright (c) 2017 shikorism.net</p> <p>Copyright (c) 2017-2019 shikorism.net</p>
<ul class="list-inline"> <ul class="list-inline">
<li class="list-inline-item"><a href="https://github.com/shibafu528" class="text-dark">Admin(@shibafu528)</a></li> <li class="list-inline-item"><a href="https://github.com/shibafu528" class="text-dark">Admin(@shibafu528)</a></li>
<li class="list-inline-item"><a href="https://github.com/shikorism/tissue" class="text-dark">GitHub</a></li> <li class="list-inline-item"><a href="https://github.com/shikorism/tissue" class="text-dark">GitHub</a></li>
</ul> </ul>
</div> </div>
</footer> </footer>
@guest
<div class="modal fade" id="ageCheckModal" tabindex="-1" role="dialog" aria-labelledby="ageCheckModalTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ageCheckModalTitle">Tissue へようこそ!</h5>
</div>
<div class="modal-body">
この先のコンテンツには暴力表現や性描写など、18歳未満の方が閲覧できないコンテンツが含まれています。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">まかせて</button>
<a href="https://cookpad.com" rel="noreferrer" class="btn btn-secondary">ごめん無理</a>
</div>
</div>
</div>
</div>
@endguest
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.js"></script>
<script type="text/javascript" src="{{ asset('js/bootstrap.min.js') }}"></script> <script type="text/javascript" src="{{ asset('js/bootstrap.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('js/tissue.js') }}"></script>
<script> <script>
$(function(){ $(function(){
@guest
if (Cookies.get('agechecked')) {
$('body').removeClass('tis-need-agecheck');
} else {
$('#ageCheckModal').modal({ backdrop: 'static' })
.on('hide.bs.modal', function() {
$('body').removeClass('tis-need-agecheck');
Cookies.set('agechecked', '1', { expires: 365 });
});
}
@endguest
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
$('.alert').alert(); $('.alert').alert();
$('.tis-page-selector').pageSelector();
@if (session('status')) @if (session('status'))
setTimeout(function () { setTimeout(function () {
$('#status').alert('close'); $('#status').alert('close');
+21
View File
@@ -0,0 +1,21 @@
@extends('layouts.base')
@section('title', $inputs['q'] . ' の検索結果')
@section('content')
<div class="container">
<h2 class="mb-4"><strong>{{ $inputs['q'] }}</strong> の検索結果</h2>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {{ Route::currentRouteName() === 'search' ? 'active' : '' }}" href="{{ route('search', $inputs) }}">チェックイン</a>
</li>
<li class="nav-item">
<a class="nav-link {{ Route::currentRouteName() === 'search.related-tag' ? 'active' : '' }}" href="{{ route('search.related-tag', $inputs) }}">関連するタグ</a>
</li>
</ul>
<div class="tab-content">
<p class="my-3 text-secondary">{{ $results->total() }} 件見つかりました</p>
@yield('tab-content')
</div>
</div>
@endsection

Some files were not shown because too many files have changed in this diff Show More