Compare commits
198 Commits
0.1.0
...
feature/ch
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f4860de95b | ||
![]() |
dc98334a6d | ||
![]() |
32b0b76032 | ||
![]() |
db3ba04091 | ||
![]() |
0400bc771c | ||
![]() |
a5b4eeee36 | ||
![]() |
f760ea7093 | ||
![]() |
0f4dfcd816 | ||
![]() |
a934a7fc35 | ||
![]() |
51f097fdf0 | ||
![]() |
24dee801ad | ||
![]() |
9f1cd607d7 | ||
![]() |
4196b1a02d | ||
![]() |
35789befc5 | ||
![]() |
32139cb9da | ||
![]() |
9244b8424d | ||
![]() |
55eb95dda8 | ||
![]() |
72e9d4e3e8 | ||
![]() |
852f1ac88c | ||
![]() |
33be0ac8ef | ||
![]() |
9f2e73e511 | ||
![]() |
09bb98876c | ||
![]() |
cedee0a20e | ||
![]() |
116dd3b798 | ||
![]() |
735bb00eba | ||
![]() |
decb1707f1 | ||
![]() |
bec7bdeb36 | ||
![]() |
7f5a4a06d9 | ||
![]() |
d7c7f86ba5 | ||
![]() |
ca212b547a | ||
![]() |
3584625b47 | ||
![]() |
1ba4999a83 | ||
![]() |
03e1c2d60c | ||
![]() |
ab0695ee8d | ||
![]() |
fcafc3c704 | ||
![]() |
7a606be3ba | ||
![]() |
b4a7ec64dd | ||
![]() |
5750eeb3a5 | ||
![]() |
b4dc07a9a3 | ||
![]() |
a77ac3f039 | ||
![]() |
eef0eac887 | ||
![]() |
7337f60491 | ||
![]() |
85e9599654 | ||
![]() |
8a919ca62a | ||
![]() |
d105568c76 | ||
![]() |
1f7723614d | ||
![]() |
41e810c788 | ||
![]() |
82af423c57 | ||
![]() |
4346e1a701 | ||
![]() |
96199c9e46 | ||
![]() |
4962244969 | ||
![]() |
c417dabff2 | ||
![]() |
d9bf673d85 | ||
![]() |
5961d3e27a | ||
![]() |
b57a272611 | ||
![]() |
cbbb2605dd | ||
![]() |
e20bb75e00 | ||
![]() |
e226f43265 | ||
![]() |
e887f2d83e | ||
![]() |
1835776a9c | ||
![]() |
57715b9a82 | ||
![]() |
e320f85c73 | ||
![]() |
4ab82ff0e2 | ||
![]() |
3c6f802b69 | ||
![]() |
895e9f4b15 | ||
![]() |
648e171a57 | ||
![]() |
dc91180dd4 | ||
![]() |
bbbffcb39e | ||
![]() |
56831c78c3 | ||
![]() |
a30919991c | ||
![]() |
8aa2e6a779 | ||
![]() |
34f45d1ce8 | ||
![]() |
6d66425fc9 | ||
![]() |
626c85c07d | ||
![]() |
b6bf1f99d8 | ||
![]() |
907eb87723 | ||
![]() |
72ec5d8d26 | ||
![]() |
ca5be696c8 | ||
![]() |
48ddac8c85 | ||
![]() |
a2580f29cc | ||
![]() |
85cc865545 | ||
![]() |
5d256519c6 | ||
![]() |
53b459740f | ||
![]() |
550d897561 | ||
![]() |
27532685ba | ||
![]() |
4654962aac | ||
![]() |
ef563f8641 | ||
![]() |
7745b68dae | ||
![]() |
e5ea0528a8 | ||
![]() |
4e1eec66be | ||
![]() |
a33a0e542c | ||
![]() |
473280d9d2 | ||
![]() |
73c697f119 | ||
![]() |
3a1dc72cf7 | ||
![]() |
b04f167709 | ||
![]() |
a3b328b55f | ||
![]() |
497c19d06d | ||
![]() |
ade52f40f4 | ||
![]() |
c38d3fa799 | ||
![]() |
2a91aac569 | ||
![]() |
f367ec212f | ||
![]() |
735c7c289a | ||
![]() |
cffe539832 | ||
![]() |
2191a96cac | ||
![]() |
64f8b47ae0 | ||
![]() |
2ca6c4c60d | ||
![]() |
0d4a61ef15 | ||
![]() |
cef23a64cb | ||
![]() |
cd26ef6236 | ||
![]() |
810eea2a59 | ||
![]() |
acb9b5821d | ||
![]() |
5f01cc3430 | ||
![]() |
938a4d6957 | ||
![]() |
a3813f19cf | ||
![]() |
faf0755ebd | ||
![]() |
a2f797cbbe | ||
![]() |
8c6cc0692c | ||
![]() |
dcf31865a1 | ||
![]() |
3dedb57fe4 | ||
![]() |
f134cbefa8 | ||
![]() |
72ab8bf101 | ||
![]() |
11836ddd43 | ||
![]() |
5c6417cdbe | ||
![]() |
0e410ef342 | ||
![]() |
d6e981ac39 | ||
![]() |
98e933b833 | ||
![]() |
6ff247acd7 | ||
![]() |
2d04ed8dd7 | ||
![]() |
d359a41033 | ||
![]() |
2299ac3fe7 | ||
![]() |
fcdb9d7aba | ||
![]() |
e6abcc4402 | ||
![]() |
b0a7504691 | ||
![]() |
a645cb497f | ||
![]() |
6105f6c860 | ||
![]() |
9eb42f1991 | ||
![]() |
dfea7f2f24 | ||
![]() |
7441c26694 | ||
![]() |
630be41833 | ||
![]() |
c4a69cccbe | ||
![]() |
cb3f060ba6 | ||
![]() |
4d7b70f9ad | ||
![]() |
3bb6bf718e | ||
![]() |
fc709a6624 | ||
![]() |
8d5363e978 | ||
![]() |
71137e9ab4 | ||
![]() |
f7a95befbe | ||
![]() |
908790b53d | ||
![]() |
20799dd757 | ||
![]() |
ca02f21812 | ||
![]() |
168ef1c5f6 | ||
![]() |
3c2e475e41 | ||
![]() |
581c1ed952 | ||
![]() |
ad5fbc7ada | ||
![]() |
cf1757319e | ||
![]() |
b39b43e705 | ||
![]() |
bcc9f3acda | ||
![]() |
911957b283 | ||
![]() |
b9e29cc283 | ||
![]() |
316b453cca | ||
![]() |
e6b28993de | ||
![]() |
e875d5da02 | ||
![]() |
233a54eb3e | ||
![]() |
515e24c4e4 | ||
![]() |
af4b60d6e1 | ||
![]() |
5bb44ab232 | ||
![]() |
874e88cc56 | ||
![]() |
bdb1640ceb | ||
![]() |
c52046d51e | ||
![]() |
69f212d705 | ||
![]() |
2441fe78b6 | ||
![]() |
9646f90ce3 | ||
![]() |
9bd22d9f77 | ||
![]() |
a0e4063c47 | ||
![]() |
0882063c0b | ||
![]() |
b4e40ab748 | ||
![]() |
6609965360 | ||
![]() |
9705c2ce5a | ||
![]() |
bd93d9ec24 | ||
![]() |
55bd35ea49 | ||
![]() |
4dc4efe10d | ||
![]() |
dfe149e969 | ||
![]() |
f51aaea94c | ||
![]() |
1dea0a077c | ||
![]() |
d143dc4d84 | ||
![]() |
e033816eab | ||
![]() |
503e8ba093 | ||
![]() |
d224e6bba4 | ||
![]() |
3b2e81818b | ||
![]() |
7ca0acacb4 | ||
![]() |
88456bc609 | ||
![]() |
0f39b502e8 | ||
![]() |
46f049c2b8 | ||
![]() |
9cdfadf12c | ||
![]() |
bc57a482be | ||
![]() |
ba53156beb | ||
![]() |
5b2427a2c9 | ||
![]() |
277ee90379 |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.idea
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
17
.env.example
17
.env.example
@@ -5,12 +5,12 @@ APP_DEBUG=true
|
||||
APP_LOG_LEVEL=debug
|
||||
APP_URL=http://localhost
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=homestead
|
||||
DB_USERNAME=homestead
|
||||
DB_PASSWORD=secret
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=tissue
|
||||
DB_USERNAME=tissue
|
||||
DB_PASSWORD=tissue
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
@@ -35,3 +35,8 @@ SPARKPOST_SECRET=
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
|
||||
# (Optional) reCAPTCHA Key
|
||||
# https://www.google.com/recaptcha
|
||||
NOCAPTCHA_SECRET=
|
||||
NOCAPTCHA_SITEKEY=
|
||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -3,3 +3,4 @@
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
*.sh text eol=lf
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -10,4 +10,8 @@ Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
*.iml
|
||||
*.iml
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.phpstorm.meta.php
|
||||
_ide_helper*.php
|
||||
|
27
.php_cs.dist
Normal file
27
.php_cs.dist
Normal 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
Dockerfile
Normal file
21
Dockerfile
Normal 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
|
73
README.md
73
README.md
@@ -1,19 +1,74 @@
|
||||
Tissue
|
||||
====
|
||||
# Tissue
|
||||
|
||||
a.k.a. shikorism.net
|
||||
|
||||
シコリズムネットにて提供している夜のライフログサービスです。
|
||||
シコリズムネットにて提供している夜のライフログサービスです。
|
||||
(思想的には [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 ファイルを設定するくらいです。
|
||||
当分、PostgreSQLから変える気はないので専用SQL等を平気で使います。
|
||||
|
||||
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。
|
||||
当分、PostgreSQL から変える気はないので専用 SQL 等を平気で使います。
|
||||
|
@@ -27,4 +27,11 @@ class Ejaculation extends Model
|
||||
{
|
||||
return $this->belongsToMany('App\Tag')->withTimestamps();
|
||||
}
|
||||
|
||||
public function textTags()
|
||||
{
|
||||
return implode(' ', $this->tags->map(function ($v) {
|
||||
return $v->name;
|
||||
})->all());
|
||||
}
|
||||
}
|
||||
|
24
app/Events/LinkDiscovered.php
Normal file
24
app/Events/LinkDiscovered.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -10,4 +10,4 @@ class Formatter extends Facade
|
||||
{
|
||||
return \App\Utilities\Formatter::class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\User;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
@@ -47,11 +47,20 @@ class RegisterController extends Controller
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
$rules = [
|
||||
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|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' => 'ユーザー名']
|
||||
);
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
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\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
|
@@ -2,19 +2,29 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Ejaculation;
|
||||
use App\Events\LinkDiscovered;
|
||||
use App\Tag;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Validator;
|
||||
use App\Ejaculation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Validator;
|
||||
|
||||
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)
|
||||
@@ -24,11 +34,11 @@ class EjaculationController extends Controller
|
||||
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
||||
}
|
||||
|
||||
Validator::make($inputs, [
|
||||
$validator = Validator::make($inputs, [
|
||||
'date' => 'required|date_format:Y/m/d',
|
||||
'time' => 'required|date_format:H:i',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'link' => 'nullable|url',
|
||||
'link' => 'nullable|url|max:2000',
|
||||
'tags' => 'nullable|string',
|
||||
])->after(function ($validator) use ($request, $inputs) {
|
||||
// 日時の重複チェック
|
||||
@@ -38,7 +48,11 @@ class EjaculationController extends Controller
|
||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||
}
|
||||
}
|
||||
})->validate();
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('checkin')->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$ejaculation = Ejaculation::create([
|
||||
'user_id' => Auth::id(),
|
||||
@@ -58,6 +72,10 @@ class EjaculationController extends Controller
|
||||
}
|
||||
$ejaculation->tags()->sync($tagIds);
|
||||
|
||||
if (!empty($ejaculation->link)) {
|
||||
event(new LinkDiscovered($ejaculation->link));
|
||||
}
|
||||
|
||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
||||
}
|
||||
|
||||
@@ -86,6 +104,7 @@ class EjaculationController extends Controller
|
||||
public function edit($id)
|
||||
{
|
||||
$ejaculation = Ejaculation::findOrFail($id);
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
Validator::make($inputs, [
|
||||
$validator = Validator::make($inputs, [
|
||||
'date' => 'required|date_format:Y/m/d',
|
||||
'time' => 'required|date_format:H:i',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'link' => 'nullable|url',
|
||||
'link' => 'nullable|url|max:2000',
|
||||
'tags' => 'nullable|string',
|
||||
])->after(function ($validator) use ($id, $request, $inputs) {
|
||||
// 日時の重複チェック
|
||||
@@ -112,7 +131,11 @@ class EjaculationController extends Controller
|
||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||
}
|
||||
}
|
||||
})->validate();
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$ejaculation->fill([
|
||||
'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);
|
||||
|
||||
if (!empty($ejaculation->link)) {
|
||||
event(new LinkDiscovered($ejaculation->link));
|
||||
}
|
||||
|
||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
||||
}
|
||||
|
||||
@@ -140,6 +167,7 @@ class EjaculationController extends Controller
|
||||
$user = User::findOrFail($ejaculation->user_id);
|
||||
$ejaculation->tags()->detach();
|
||||
$ejaculation->delete();
|
||||
|
||||
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,31 @@ class HomeController extends Controller
|
||||
$categories = Information::CATEGORIES;
|
||||
|
||||
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')
|
||||
->where('users.is_protected', false)
|
||||
@@ -44,10 +69,10 @@ class HomeController extends Controller
|
||||
->orderBy('ejaculations.ejaculated_date', 'desc')
|
||||
->select('ejaculations.*')
|
||||
->with('user', 'tags')
|
||||
->take(5)
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
return view('home')->with(compact('informations', 'categories', 'publicLinkedEjaculations'));
|
||||
return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
|
||||
} else {
|
||||
return view('guest')->with(compact('informations', 'categories'));
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ class InfoController extends Controller
|
||||
->orderByDesc('pinned')
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('info.index')->with([
|
||||
'informations' => $informations,
|
||||
'categories' => Information::CATEGORIES
|
||||
@@ -23,6 +24,7 @@ class InfoController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$information = Information::findOrFail($id);
|
||||
|
||||
return view('info.show')->with([
|
||||
'info' => $information,
|
||||
'category' => Information::CATEGORIES[$information->category]
|
||||
|
43
app/Http/Controllers/SearchController.php
Normal file
43
app/Http/Controllers/SearchController.php
Normal 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'));
|
||||
}
|
||||
}
|
64
app/Http/Controllers/SettingController.php
Normal file
64
app/Http/Controllers/SettingController.php
Normal file
@@ -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);
|
||||
// }
|
||||
}
|
@@ -21,7 +21,8 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
// チェックインの取得
|
||||
$query = Ejaculation::select(DB::raw(<<<'SQL'
|
||||
$query = Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
id,
|
||||
ejaculated_date,
|
||||
note,
|
||||
@@ -39,7 +40,21 @@ SQL
|
||||
->with('tags')
|
||||
->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)
|
||||
@@ -49,7 +64,8 @@ SQL
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$groupByDay = Ejaculation::select(DB::raw(<<<'SQL'
|
||||
$groupByDay = Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
||||
count(*) AS "count"
|
||||
SQL
|
||||
@@ -59,9 +75,22 @@ SQL
|
||||
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||
->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 = [];
|
||||
$monthlySum = [];
|
||||
$yearlySum = [];
|
||||
$dowSum = array_fill(0, 7, 0);
|
||||
$hourlySum = array_fill(0, 24, 0);
|
||||
|
||||
// 年間グラフ用の配列初期化
|
||||
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++) {
|
||||
$monthlySum[$month->format('Y/m')] = 0;
|
||||
$month->addMonth();
|
||||
@@ -85,12 +114,18 @@ SQL
|
||||
|
||||
$dailySum[$date->timestamp] = $data->count;
|
||||
$yearlySum[$date->year] += $data->count;
|
||||
$dowSum[$date->dayOfWeek] += $data->count;
|
||||
if (isset($monthlySum[$yearAndMonth])) {
|
||||
$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)
|
||||
@@ -101,7 +136,8 @@ SQL
|
||||
}
|
||||
|
||||
// チェックインの取得
|
||||
$query = Ejaculation::select(DB::raw(<<<'SQL'
|
||||
$query = Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
id,
|
||||
ejaculated_date,
|
||||
note,
|
||||
|
@@ -18,7 +18,7 @@ class RedirectIfAuthenticated
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect('/home');
|
||||
return redirect()->route('home');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
@@ -7,7 +7,7 @@ use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileComposer
|
||||
class ProfileStatsComposer
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -58,4 +58,4 @@ SQL
|
||||
|
||||
$view->with(compact('latestEjaculation', 'currentSession', 'summary'));
|
||||
}
|
||||
}
|
||||
}
|
63
app/Listeners/LinkCollector.php
Normal file
63
app/Listeners/LinkCollector.php
Normal 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
app/Metadata.php
Normal file
17
app/Metadata.php
Normal 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'];
|
||||
}
|
80
app/MetadataResolver/ActivityPubResolver.php
Normal file
80
app/MetadataResolver/ActivityPubResolver.php
Normal file
@@ -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
app/MetadataResolver/CienResolver.php
Normal file
43
app/MetadataResolver/CienResolver.php
Normal 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
app/MetadataResolver/DLsiteResolver.php
Normal file
36
app/MetadataResolver/DLsiteResolver.php
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
57
app/MetadataResolver/DeviantArtResolver.php
Normal file
57
app/MetadataResolver/DeviantArtResolver.php
Normal file
@@ -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
app/MetadataResolver/FantiaResolver.php
Normal file
55
app/MetadataResolver/FantiaResolver.php
Normal 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
app/MetadataResolver/FanzaResolver.php
Normal file
36
app/MetadataResolver/FanzaResolver.php
Normal 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
app/MetadataResolver/IwaraResolver.php
Normal file
69
app/MetadataResolver/IwaraResolver.php
Normal 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
app/MetadataResolver/KomifloResolver.php
Normal file
41
app/MetadataResolver/KomifloResolver.php
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
43
app/MetadataResolver/MelonbooksResolver.php
Normal file
43
app/MetadataResolver/MelonbooksResolver.php
Normal file
@@ -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
app/MetadataResolver/Metadata.php
Normal file
14
app/MetadataResolver/Metadata.php
Normal 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
app/MetadataResolver/MetadataResolver.php
Normal file
108
app/MetadataResolver/MetadataResolver.php
Normal 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
app/MetadataResolver/NarouResolver.php
Normal file
60
app/MetadataResolver/NarouResolver.php
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
39
app/MetadataResolver/NicoSeigaResolver.php
Normal file
39
app/MetadataResolver/NicoSeigaResolver.php
Normal file
@@ -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
app/MetadataResolver/NijieResolver.php
Normal file
56
app/MetadataResolver/NijieResolver.php
Normal 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
app/MetadataResolver/OGPResolver.php
Normal file
64
app/MetadataResolver/OGPResolver.php
Normal 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
app/MetadataResolver/Parser.php
Normal file
8
app/MetadataResolver/Parser.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
interface Parser
|
||||
{
|
||||
public function parse(string $body): Metadata;
|
||||
}
|
42
app/MetadataResolver/PatreonResolver.php
Normal file
42
app/MetadataResolver/PatreonResolver.php
Normal 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
app/MetadataResolver/PixivResolver.php
Normal file
86
app/MetadataResolver/PixivResolver.php
Normal 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
app/MetadataResolver/Resolver.php
Normal file
8
app/MetadataResolver/Resolver.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
interface Resolver
|
||||
{
|
||||
public function resolve(string $url): Metadata;
|
||||
}
|
37
app/MetadataResolver/ToranoanaResolver.php
Normal file
37
app/MetadataResolver/ToranoanaResolver.php
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\MetadataResolver\MetadataResolver;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Parsedown;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -13,7 +16,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
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()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(MetadataResolver::class, function ($app) {
|
||||
return new MetadataResolver();
|
||||
});
|
||||
$this->app->singleton('parsedown', function () {
|
||||
return Parsedown::instance();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -13,9 +13,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
'App\Events\Event' => [
|
||||
'App\Listeners\EventListener',
|
||||
],
|
||||
'App\Events\LinkDiscovered' => [
|
||||
'App\Listeners\LinkCollector'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\ViewComposers\ProfileComposer;
|
||||
use App\Http\ViewComposers\ProfileStatsComposer;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@@ -15,7 +15,7 @@ class ViewComposerServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
View::composer('components.profile', ProfileComposer::class);
|
||||
View::composer('components.profile-stats', ProfileStatsComposer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class User extends Authenticatable
|
||||
@@ -36,9 +36,10 @@ class User extends Authenticatable
|
||||
* @param int $size 画像サイズ
|
||||
* @return string Gravatar 画像URL
|
||||
*/
|
||||
public function getProfileImageUrl($size = 30) : string
|
||||
public function getProfileImageUrl($size = 30): string
|
||||
{
|
||||
$hash = md5(strtolower(trim($this->email)));
|
||||
|
||||
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size;
|
||||
}
|
||||
|
||||
|
@@ -24,6 +24,7 @@ class Formatter
|
||||
$days = floor($value / 86400);
|
||||
$hours = floor($value % 86400 / 3600);
|
||||
$minutes = floor($value % 3600 / 60);
|
||||
|
||||
return "{$days}日 {$hours}時間 {$minutes}分";
|
||||
}
|
||||
|
||||
@@ -36,4 +37,42 @@ class Formatter
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -5,19 +5,24 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"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",
|
||||
"laravel/framework": "5.5.*",
|
||||
"laravel/tinker": "~1.0",
|
||||
"misd/linkify": "^1.1",
|
||||
"parsedown/laravel": "~1.0"
|
||||
"misd/linkify": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.1",
|
||||
"barryvdh/laravel-ide-helper": "^2.5",
|
||||
"filp/whoops": "~2.0",
|
||||
"friendsofphp/php-cs-fixer": "^2.14",
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "~1.0",
|
||||
"phpunit/phpunit": "~6.0"
|
||||
"phpunit/phpunit": "~6.0",
|
||||
"symfony/thanks": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
@@ -42,6 +47,9 @@
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover"
|
||||
],
|
||||
"fix": [
|
||||
"php-cs-fixer fix"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
2603
composer.lock
generated
2603
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUsersTable extends Migration
|
||||
{
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePasswordResetsTable extends Migration
|
||||
{
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateEjaculationsTable extends Migration
|
||||
{
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateInformationTable extends Migration
|
||||
{
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddLinkToEjaculations extends Migration
|
||||
{
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
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
dist/bin/tissue-entrypoint.sh
vendored
Executable file
8
dist/bin/tissue-entrypoint.sh
vendored
Executable 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
dist/php.d/99-xdebug.ini
vendored
Normal file
4
dist/php.d/99-xdebug.ini
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
; Dockerでのデバッグ用設定
|
||||
zend_extension=xdebug.so
|
||||
xdebug.remote_enable=true
|
||||
xdebug.remote_host=host.docker.internal
|
6
docker-compose.debug.yml
Normal file
6
docker-compose.debug.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
db:
|
||||
ports:
|
||||
- 5432:5432
|
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal 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:
|
5
prepare.sh
Executable file
5
prepare.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# https://laravel.com/docs/5.5/deployment
|
||||
composer install --optimize-autoloader
|
||||
php artisan config:cache
|
1353
public/css/bootstrap-grid.css
vendored
1353
public/css/bootstrap-grid.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
public/css/bootstrap-grid.min.css
vendored
7
public/css/bootstrap-grid.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
330
public/css/bootstrap-reboot.css
vendored
330
public/css/bootstrap-reboot.css
vendored
@@ -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
8
public/css/bootstrap-reboot.min.css
vendored
8
public/css/bootstrap-reboot.min.css
vendored
@@ -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 */
|
File diff suppressed because one or more lines are too long
8185
public/css/bootstrap.css
vendored
8185
public/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
8
public/css/bootstrap.min.css
vendored
8
public/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
39
public/css/tissue.css
vendored
39
public/css/tissue.css
vendored
@@ -15,6 +15,25 @@
|
||||
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 {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
@@ -27,4 +46,24 @@
|
||||
|
||||
.timeline-action-item {
|
||||
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;
|
||||
}
|
||||
}
|
7336
public/js/bootstrap.js
vendored
7336
public/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
11
public/js/bootstrap.min.js
vendored
11
public/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
55
public/js/tissue.js
vendored
Normal file
55
public/js/tissue.js
vendored
Normal 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);
|
@@ -99,6 +99,10 @@ return [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
'g-recaptcha-response' => [
|
||||
'required' => '「私はロボットではありません」にチェックを入れてください。',
|
||||
'captcha' => 'reCAPTCHAチェックに失敗しました。何度試しても解決しない場合、管理者にお問い合わせください。',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -117,4 +121,4 @@ return [
|
||||
'password' => 'パスワード',
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', 'ログイン')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>ログイン</h2>
|
||||
@@ -11,7 +13,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<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'))
|
||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||
@@ -25,12 +27,9 @@
|
||||
<div class="invalid-feedback">{{ $errors->first('password') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input id="remember" name="rememver" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
|
||||
<span class="custom-control-indicator"></span>
|
||||
<span class="custom-control-description">保存する</span>
|
||||
</label>
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="remember" name="remember" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="remember">保存する</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="submit">ログイン</button>
|
||||
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', 'パスワードの再発行')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>パスワードの再発行</h2>
|
||||
@@ -14,7 +16,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<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'))
|
||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', 'パスワードの再発行')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>パスワードの再発行</h2>
|
||||
@@ -16,7 +18,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<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'))
|
||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||
|
@@ -1,9 +1,21 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', '新規登録')
|
||||
|
||||
@push('head')
|
||||
@if (!empty(config('captcha.secret')))
|
||||
{!! NoCaptcha::renderJs() !!}
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>新規登録</h2>
|
||||
<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="col-lg-6">
|
||||
<form method="post" action="{{ route('register') }}">
|
||||
@@ -19,7 +31,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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'))
|
||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||
@@ -41,22 +53,26 @@
|
||||
<h6 class="mb-3">プライバシーに関するオプション (全て任意です)</h6>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
|
||||
<span class="custom-control-indicator"></span>
|
||||
<span class="custom-control-description">全てのチェックイン履歴を非公開にする</span>
|
||||
</label>
|
||||
<div class="custom-control custom-checkbox mb-2">
|
||||
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="protected">全てのチェックイン履歴を非公開にする</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
|
||||
<span class="custom-control-indicator"></span>
|
||||
<span class="custom-control-description">匿名での統計にチェックインデータを利用することに同意します</span>
|
||||
</label>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="accept-analytics">匿名での統計にチェックインデータを利用することに同意します</label>
|
||||
</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">
|
||||
<button class="btn btn-primary btn-lg" type="submit">登録</button>
|
||||
|
9
resources/views/components/card.blade.php
Normal file
9
resources/views/components/card.blade.php
Normal file
@@ -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>
|
15
resources/views/components/profile-stats.blade.php
Normal file
15
resources/views/components/profile-stats.blade.php
Normal file
@@ -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>
|
@@ -1,6 +1,6 @@
|
||||
<div class="card mb-4">
|
||||
<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">
|
||||
<a class="text-dark" href="{{ route('user.profile', ['name' => $user->name]) }}">{{ $user->display_name }}</a>
|
||||
</h4>
|
||||
@@ -11,22 +11,28 @@
|
||||
@endif
|
||||
</h6>
|
||||
|
||||
@if (!$user->is_protected)
|
||||
<h6 class="font-weight-bold mt-4"><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
|
||||
{{-- Bio --}}
|
||||
@if (!empty($user->bio))
|
||||
<p class="card-text mt-3 mb-0">
|
||||
{!! Formatter::linkify(nl2br(e($user->bio))) !!}
|
||||
</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>
|
||||
{{-- URL --}}
|
||||
@if (!empty($user->url))
|
||||
<p class="card-text d-flex mt-3">
|
||||
<span class="oi oi-link-intact mr-1 mt-1"></span>
|
||||
<a href="{{ $user->url }}" rel="me nofollow noopener" target="_blank" class="text-truncate">{{ preg_replace('~\Ahttps?://~', '', $user->url) }}</a>
|
||||
</p>
|
||||
@endif
|
||||
</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
|
||||
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', 'チェックイン')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>今致してる?</h2>
|
||||
@@ -13,7 +15,7 @@
|
||||
<div class="form-group col-sm-6">
|
||||
<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' : '' }}"
|
||||
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'))
|
||||
<div class="invalid-feedback">{{ $errors->first('date') }}</div>
|
||||
@@ -22,7 +24,7 @@
|
||||
<div class="form-group col-sm-6">
|
||||
<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' : '' }}"
|
||||
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'))
|
||||
<div class="invalid-feedback">{{ $errors->first('time') }}</div>
|
||||
@@ -36,9 +38,9 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<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>
|
||||
<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>
|
||||
<input id="tagInput" type="text" style="outline: 0; border: 0;">
|
||||
</div>
|
||||
@@ -54,7 +56,7 @@
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-12">
|
||||
<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">
|
||||
オカズのURLを貼り付けて登録することができます。
|
||||
</small>
|
||||
@@ -62,11 +64,13 @@
|
||||
<div class="invalid-feedback">{{ $errors->first('link') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@component('components.card', ['link' => null])
|
||||
@endcomponent
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-12">
|
||||
<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">
|
||||
最大 500 文字
|
||||
</small>
|
||||
@@ -78,13 +82,10 @@
|
||||
<div class="form-row mt-4">
|
||||
<p>オプション</p>
|
||||
<div class="form-group col-sm-12">
|
||||
<div class="form-check">
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') ? 'checked' : '' }}>
|
||||
<span class="custom-control-indicator"></span>
|
||||
<span class="custom-control-description">
|
||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||
</span>
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') || $defaults['is_private'] ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="isPrivate">
|
||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,4 +151,60 @@
|
||||
$('#tagInput').focus();
|
||||
});
|
||||
</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
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', 'チェックインの修正')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>チェックインの修正</h2>
|
||||
@@ -37,9 +39,9 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<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>
|
||||
<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>
|
||||
<input id="tagInput" type="text" style="outline: 0; border: 0;">
|
||||
</div>
|
||||
@@ -52,21 +54,10 @@
|
||||
@endif
|
||||
</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-group col-sm-12">
|
||||
<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">
|
||||
オカズのURLを貼り付けて登録することができます。
|
||||
</small>
|
||||
@@ -90,13 +81,10 @@
|
||||
<div class="form-row mt-4">
|
||||
<p>オプション</p>
|
||||
<div class="form-group col-sm-12">
|
||||
<div class="form-check">
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
|
||||
<span class="custom-control-indicator"></span>
|
||||
<span class="custom-control-description">
|
||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||
</span>
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="isPrivate" 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="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,11 @@
|
||||
@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')
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
@@ -26,12 +32,13 @@
|
||||
<!-- span -->
|
||||
<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>
|
||||
@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="#" 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>
|
||||
<!-- tags -->
|
||||
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
||||
@@ -40,24 +47,19 @@
|
||||
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
|
||||
@endif
|
||||
@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
|
||||
</p>
|
||||
@endif
|
||||
<!-- okazu link -->
|
||||
@if (!empty($ejaculation->link))
|
||||
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;">
|
||||
<a class="text-dark card-link" href="{{ $ejaculation->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>
|
||||
<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>
|
||||
<div class="row mx-0">
|
||||
@component('components.card', ['link' => $ejaculation->link])
|
||||
@endcomponent
|
||||
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
|
||||
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
<!-- note -->
|
||||
@if (!empty($ejaculation->note))
|
||||
@@ -103,42 +105,8 @@
|
||||
form.submit();
|
||||
});
|
||||
|
||||
$('.link-card').each(function () {
|
||||
var $this = $(this);
|
||||
$.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');
|
||||
}
|
||||
});
|
||||
$('.link-card').linkCard({
|
||||
endpoint: '{{ url('/api/checkin/card') }}'
|
||||
});
|
||||
</script>
|
||||
@endpush
|
@@ -31,6 +31,9 @@
|
||||
<div class="list-group list-group-flush">
|
||||
@foreach($informations as $info)
|
||||
<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>
|
||||
</a>
|
||||
@endforeach
|
||||
|
@@ -7,114 +7,135 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
@component('components.profile', ['user' => Auth::user()])
|
||||
@endcomponent
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<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]) }}">@{{ 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
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4">
|
||||
<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)
|
||||
<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>
|
||||
</a>
|
||||
@endforeach
|
||||
<a href="{{ route('info') }}" class="list-group-item text-right">お知らせ一覧 »</a>
|
||||
</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>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach ($publicLinkedEjaculations as $ejaculation)
|
||||
<li class="list-group-item pt-3 pb-3">
|
||||
<!-- span -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<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"> @{{ $ejaculation->user->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>
|
||||
</h5>
|
||||
</div>
|
||||
<!-- tags -->
|
||||
@if ($ejaculation->tags->isNotEmpty())
|
||||
<p class="mb-2">
|
||||
@foreach ($ejaculation->tags as $tag)
|
||||
<span class="badge badge-secondary"><span class="oi oi-tag"></span> {{ $tag->name }}</span>
|
||||
@endforeach
|
||||
</p>
|
||||
@endif
|
||||
<!-- okazu link -->
|
||||
@if (!empty($ejaculation->link))
|
||||
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;">
|
||||
<a class="text-dark card-link" href="{{ $ejaculation->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>
|
||||
<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>
|
||||
@endif
|
||||
<!-- note -->
|
||||
@if (!empty($ejaculation->note))
|
||||
<p class="mb-0 tis-word-wrap">
|
||||
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
|
||||
</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
<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)
|
||||
<li class="list-group-item no-side-border pt-3 pb-3 tis-word-wrap">
|
||||
<!-- span -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<h5>
|
||||
<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>
|
||||
</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>
|
||||
<!-- tags -->
|
||||
@if ($ejaculation->tags->isNotEmpty())
|
||||
<p class="mb-2">
|
||||
@foreach ($ejaculation->tags as $tag)
|
||||
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
|
||||
@endforeach
|
||||
</p>
|
||||
@endif
|
||||
<!-- okazu link -->
|
||||
@if (!empty($ejaculation->link))
|
||||
<div class="row mx-0">
|
||||
@component('components.card', ['link' => $ejaculation->link])
|
||||
@endcomponent
|
||||
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
|
||||
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
<!-- note -->
|
||||
@if (!empty($ejaculation->note))
|
||||
<p class="mb-0 tis-word-wrap">
|
||||
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
|
||||
</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('script')
|
||||
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/chart.js@2.7.1/dist/Chart.min.js"></script>
|
||||
<script>
|
||||
$('.link-card').each(function () {
|
||||
var $this = $(this);
|
||||
$.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');
|
||||
$('.link-card').linkCard({
|
||||
endpoint: '{{ url('/api/checkin/card') }}'
|
||||
});
|
||||
|
||||
if (data.title === '') {
|
||||
$title.hide();
|
||||
} else {
|
||||
$title.text(data.title);
|
||||
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
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
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>
|
||||
@endpush
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', 'お知らせ')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<h2>サイトからのお知らせ</h2>
|
||||
@@ -7,26 +9,13 @@
|
||||
<div class="list-group">
|
||||
@foreach($informations as $info)
|
||||
<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>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<ul class="pagination 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">«</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">»</span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ $informations->links(null, ['className' => 'mt-4 justify-content-center']) }}
|
||||
</div>
|
||||
@endsection
|
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.base')
|
||||
|
||||
@section('title', $category['label'] . ': ' . $info->title)
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<nav aria-label="breadcrumb" role="navigation">
|
||||
@@ -9,7 +11,12 @@
|
||||
</ol>
|
||||
</nav>
|
||||
<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)
|
||||
</div>
|
||||
@endsection
|
@@ -6,7 +6,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Tissue') }}</title>
|
||||
@hasSection('title')
|
||||
<title>@yield('title') - {{ config('app.name', 'Tissue') }}</title>
|
||||
@else
|
||||
<title>{{ config('app.name', 'Tissue') }}</title>
|
||||
@endif
|
||||
|
||||
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ asset('css/open-iconic-bootstrap.min.css') }}" rel="stylesheet">
|
||||
@@ -14,7 +18,18 @@
|
||||
|
||||
@stack('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'}}">
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
|
||||
{{ csrf_field() }}
|
||||
@@ -44,24 +59,43 @@
|
||||
<a class="nav-link" href="{{ route('ranking') }}">ランキング</a>
|
||||
</li>--}}
|
||||
</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">
|
||||
<li class="nav-item dropdown">
|
||||
<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">
|
||||
{{ Auth::user()->display_name }} さん
|
||||
<img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top">
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
{{--<a href="#" class="dropdown-item">設定</a>--}}
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
|
||||
<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>@{{ 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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline">
|
||||
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
|
||||
</form>
|
||||
@endauth
|
||||
@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>
|
||||
</form>
|
||||
@endguest
|
||||
@@ -81,21 +115,52 @@
|
||||
@yield('content')
|
||||
<footer class="tis-footer mt-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">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</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://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/tissue.js') }}"></script>
|
||||
<script>
|
||||
$(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();
|
||||
$('.alert').alert();
|
||||
$('.tis-page-selector').pageSelector();
|
||||
@if (session('status'))
|
||||
setTimeout(function () {
|
||||
$('#status').alert('close');
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user