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_LOG_LEVEL=debug
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=pgsql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=db
|
||||||
DB_PORT=3306
|
DB_PORT=5432
|
||||||
DB_DATABASE=homestead
|
DB_DATABASE=tissue
|
||||||
DB_USERNAME=homestead
|
DB_USERNAME=tissue
|
||||||
DB_PASSWORD=secret
|
DB_PASSWORD=tissue
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
BROADCAST_DRIVER=log
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
@@ -35,3 +35,8 @@ SPARKPOST_SECRET=
|
|||||||
PUSHER_APP_ID=
|
PUSHER_APP_ID=
|
||||||
PUSHER_APP_KEY=
|
PUSHER_APP_KEY=
|
||||||
PUSHER_APP_SECRET=
|
PUSHER_APP_SECRET=
|
||||||
|
|
||||||
|
# (Optional) reCAPTCHA Key
|
||||||
|
# https://www.google.com/recaptcha
|
||||||
|
NOCAPTCHA_SECRET=
|
||||||
|
NOCAPTCHA_SITEKEY=
|
||||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -3,3 +3,4 @@
|
|||||||
*.scss linguist-vendored
|
*.scss linguist-vendored
|
||||||
*.js linguist-vendored
|
*.js linguist-vendored
|
||||||
CHANGELOG.md export-ignore
|
CHANGELOG.md export-ignore
|
||||||
|
*.sh text eol=lf
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -10,4 +10,8 @@ Homestead.yaml
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.env
|
.env
|
||||||
*.iml
|
*.iml
|
||||||
|
.php_cs
|
||||||
|
.php_cs.cache
|
||||||
|
.phpstorm.meta.php
|
||||||
|
_ide_helper*.php
|
||||||
|
27
.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
|
a.k.a. shikorism.net
|
||||||
|
|
||||||
シコリズムネットにて提供している夜のライフログサービスです。
|
シコリズムネットにて提供している夜のライフログサービスです。
|
||||||
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
|
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
|
||||||
|
|
||||||
## 構成
|
## 構成
|
||||||
* Laravel 5.5
|
|
||||||
* Bootstrap 4.0
|
- Laravel 5.5
|
||||||
|
- Bootstrap 4.2.1
|
||||||
|
|
||||||
## 実行環境
|
## 実行環境
|
||||||
* PHP 7.1
|
|
||||||
* PostgreSQL 9.6
|
- PHP 7.1
|
||||||
|
- PostgreSQL 9.6
|
||||||
|
|
||||||
|
## 開発環境の構築
|
||||||
|
|
||||||
|
Docker を用いた開発環境の構築方法です。
|
||||||
|
|
||||||
|
1. `.env` ファイルを用意します。`.env.example` をコピーすることで用意ができます。
|
||||||
|
|
||||||
|
2. Docker イメージをビルドします
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Docker コンテナを起動します。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Composer を使い必要なライブラリをインストールします。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 暗号化キーの作成と、データベースのマイグレーションを行います。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web php artisan key:generate
|
||||||
|
docker-compose exec web php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
6. ファイルに書き込めるように権限を設定します。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web chown -R www-data /var/www/html
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 最後に `.env` を読み込み直すために起動し直します。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
これで準備は完了です。Tissue が動いていれば `http://localhost:4545/` でアクセスができます。
|
||||||
|
|
||||||
|
## デバッグ実行
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
で起動することにより、DB のポート`5432`を開放してホストマシンから接続できるようになります。
|
||||||
|
|
||||||
## 環境構築上の諸注意
|
## 環境構築上の諸注意
|
||||||
* 初版時点では、DBサーバとしてPostgreSQLを使うよう .env ファイルを設定するくらいです。
|
|
||||||
当分、PostgreSQLから変える気はないので専用SQL等を平気で使います。
|
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。
|
||||||
|
当分、PostgreSQL から変える気はないので専用 SQL 等を平気で使います。
|
||||||
|
@@ -27,4 +27,11 @@ class Ejaculation extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany('App\Tag')->withTimestamps();
|
return $this->belongsToMany('App\Tag')->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function textTags()
|
||||||
|
{
|
||||||
|
return implode(' ', $this->tags->map(function ($v) {
|
||||||
|
return $v->name;
|
||||||
|
})->all());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
24
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;
|
return \App\Utilities\Formatter::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\User;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use App\User;
|
||||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
class RegisterController extends Controller
|
class RegisterController extends Controller
|
||||||
{
|
{
|
||||||
@@ -47,11 +47,20 @@ class RegisterController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function validator(array $data)
|
protected function validator(array $data)
|
||||||
{
|
{
|
||||||
return Validator::make($data, [
|
$rules = [
|
||||||
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users',
|
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users',
|
||||||
'email' => 'required|string|email|max:255|unique:users',
|
'email' => 'required|string|email|max:255|unique:users',
|
||||||
'password' => 'required|string|min:6|confirmed',
|
'password' => 'required|string|min:6|confirmed'
|
||||||
],
|
];
|
||||||
|
|
||||||
|
// reCAPTCHAのキーが設定されている場合、判定を有効化
|
||||||
|
if (!empty(config('captcha.secret'))) {
|
||||||
|
$rules['g-recaptcha-response'] = 'required|captcha';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Validator::make(
|
||||||
|
$data,
|
||||||
|
$rules,
|
||||||
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
|
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
|
||||||
['name' => 'ユーザー名']
|
['name' => 'ユーザー名']
|
||||||
);
|
);
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
|
||||||
class Controller extends BaseController
|
class Controller extends BaseController
|
||||||
{
|
{
|
||||||
|
@@ -2,19 +2,29 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use App\Events\LinkDiscovered;
|
||||||
use App\Tag;
|
use App\Tag;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Validator;
|
|
||||||
use App\Ejaculation;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
class EjaculationController extends Controller
|
class EjaculationController extends Controller
|
||||||
{
|
{
|
||||||
public function create()
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
return view('ejaculation.checkin');
|
$defaults = [
|
||||||
|
'date' => $request->input('date', date('Y/m/d')),
|
||||||
|
'time' => $request->input('time', date('H:i')),
|
||||||
|
'link' => $request->input('link', ''),
|
||||||
|
'tags' => $request->input('tags', ''),
|
||||||
|
'note' => $request->input('note', ''),
|
||||||
|
'is_private' => $request->input('is_private', 0) == 1
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('ejaculation.checkin')->with('defaults', $defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@@ -24,11 +34,11 @@ class EjaculationController extends Controller
|
|||||||
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Validator::make($inputs, [
|
$validator = Validator::make($inputs, [
|
||||||
'date' => 'required|date_format:Y/m/d',
|
'date' => 'required|date_format:Y/m/d',
|
||||||
'time' => 'required|date_format:H:i',
|
'time' => 'required|date_format:H:i',
|
||||||
'note' => 'nullable|string|max:500',
|
'note' => 'nullable|string|max:500',
|
||||||
'link' => 'nullable|url',
|
'link' => 'nullable|url|max:2000',
|
||||||
'tags' => 'nullable|string',
|
'tags' => 'nullable|string',
|
||||||
])->after(function ($validator) use ($request, $inputs) {
|
])->after(function ($validator) use ($request, $inputs) {
|
||||||
// 日時の重複チェック
|
// 日時の重複チェック
|
||||||
@@ -38,7 +48,11 @@ class EjaculationController extends Controller
|
|||||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})->validate();
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('checkin')->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
$ejaculation = Ejaculation::create([
|
$ejaculation = Ejaculation::create([
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
@@ -58,6 +72,10 @@ class EjaculationController extends Controller
|
|||||||
}
|
}
|
||||||
$ejaculation->tags()->sync($tagIds);
|
$ejaculation->tags()->sync($tagIds);
|
||||||
|
|
||||||
|
if (!empty($ejaculation->link)) {
|
||||||
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +104,7 @@ class EjaculationController extends Controller
|
|||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$ejaculation = Ejaculation::findOrFail($id);
|
$ejaculation = Ejaculation::findOrFail($id);
|
||||||
|
|
||||||
return view('ejaculation.edit')->with(compact('ejaculation'));
|
return view('ejaculation.edit')->with(compact('ejaculation'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +117,11 @@ class EjaculationController extends Controller
|
|||||||
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Validator::make($inputs, [
|
$validator = Validator::make($inputs, [
|
||||||
'date' => 'required|date_format:Y/m/d',
|
'date' => 'required|date_format:Y/m/d',
|
||||||
'time' => 'required|date_format:H:i',
|
'time' => 'required|date_format:H:i',
|
||||||
'note' => 'nullable|string|max:500',
|
'note' => 'nullable|string|max:500',
|
||||||
'link' => 'nullable|url',
|
'link' => 'nullable|url|max:2000',
|
||||||
'tags' => 'nullable|string',
|
'tags' => 'nullable|string',
|
||||||
])->after(function ($validator) use ($id, $request, $inputs) {
|
])->after(function ($validator) use ($id, $request, $inputs) {
|
||||||
// 日時の重複チェック
|
// 日時の重複チェック
|
||||||
@@ -112,7 +131,11 @@ class EjaculationController extends Controller
|
|||||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})->validate();
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
$ejaculation->fill([
|
$ejaculation->fill([
|
||||||
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
||||||
@@ -131,6 +154,10 @@ class EjaculationController extends Controller
|
|||||||
}
|
}
|
||||||
$ejaculation->tags()->sync($tagIds);
|
$ejaculation->tags()->sync($tagIds);
|
||||||
|
|
||||||
|
if (!empty($ejaculation->link)) {
|
||||||
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +167,7 @@ class EjaculationController extends Controller
|
|||||||
$user = User::findOrFail($ejaculation->user_id);
|
$user = User::findOrFail($ejaculation->user_id);
|
||||||
$ejaculation->tags()->detach();
|
$ejaculation->tags()->detach();
|
||||||
$ejaculation->delete();
|
$ejaculation->delete();
|
||||||
|
|
||||||
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -36,6 +36,31 @@ class HomeController extends Controller
|
|||||||
$categories = Information::CATEGORIES;
|
$categories = Information::CATEGORIES;
|
||||||
|
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
|
// チェックイン動向グラフ用のデータ取得
|
||||||
|
$groupByDay = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
|
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
||||||
|
count(*) AS "count"
|
||||||
|
SQL
|
||||||
|
))
|
||||||
|
->join('users', function ($join) {
|
||||||
|
$join->on('users.id', '=', 'ejaculations.user_id')
|
||||||
|
->where('users.accept_analytics', true);
|
||||||
|
})
|
||||||
|
->where('ejaculated_date', '>=', now()->subDays(30))
|
||||||
|
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
|
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
|
->get()
|
||||||
|
->mapWithKeys(function ($item) {
|
||||||
|
return [$item['date'] => $item['count']];
|
||||||
|
});
|
||||||
|
$globalEjaculationCounts = [];
|
||||||
|
$day = Carbon::now()->subDays(29);
|
||||||
|
for ($i = 0; $i < 30; $i++) {
|
||||||
|
$globalEjaculationCounts[$day->format('Y/m/d') . ' の総チェックイン数'] = $groupByDay[$day->format('Y/m/d')] ?? 0;
|
||||||
|
$day->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
// お惣菜コーナー用のデータ取得
|
// お惣菜コーナー用のデータ取得
|
||||||
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
|
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
|
||||||
->where('users.is_protected', false)
|
->where('users.is_protected', false)
|
||||||
@@ -44,10 +69,10 @@ class HomeController extends Controller
|
|||||||
->orderBy('ejaculations.ejaculated_date', 'desc')
|
->orderBy('ejaculations.ejaculated_date', 'desc')
|
||||||
->select('ejaculations.*')
|
->select('ejaculations.*')
|
||||||
->with('user', 'tags')
|
->with('user', 'tags')
|
||||||
->take(5)
|
->take(10)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('home')->with(compact('informations', 'categories', 'publicLinkedEjaculations'));
|
return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
|
||||||
} else {
|
} else {
|
||||||
return view('guest')->with(compact('informations', 'categories'));
|
return view('guest')->with(compact('informations', 'categories'));
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ class InfoController extends Controller
|
|||||||
->orderByDesc('pinned')
|
->orderByDesc('pinned')
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
return view('info.index')->with([
|
return view('info.index')->with([
|
||||||
'informations' => $informations,
|
'informations' => $informations,
|
||||||
'categories' => Information::CATEGORIES
|
'categories' => Information::CATEGORIES
|
||||||
@@ -23,6 +24,7 @@ class InfoController extends Controller
|
|||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$information = Information::findOrFail($id);
|
$information = Information::findOrFail($id);
|
||||||
|
|
||||||
return view('info.show')->with([
|
return view('info.show')->with([
|
||||||
'info' => $information,
|
'info' => $information,
|
||||||
'category' => Information::CATEGORIES[$information->category]
|
'category' => Information::CATEGORIES[$information->category]
|
||||||
|
43
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,
|
id,
|
||||||
ejaculated_date,
|
ejaculated_date,
|
||||||
note,
|
note,
|
||||||
@@ -39,7 +40,21 @@ SQL
|
|||||||
->with('tags')
|
->with('tags')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
return view('user.profile')->with(compact('user', 'ejaculations'));
|
// よく使っているタグ
|
||||||
|
$tagsQuery = DB::table('ejaculations')
|
||||||
|
->join('ejaculation_tag', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id')
|
||||||
|
->join('tags', 'ejaculation_tag.tag_id', '=', 'tags.id')
|
||||||
|
->selectRaw('tags.name, count(*) as count')
|
||||||
|
->where('ejaculations.user_id', $user->id);
|
||||||
|
if (!Auth::check() || $user->id !== Auth::id()) {
|
||||||
|
$tagsQuery = $tagsQuery->where('ejaculations.is_private', false);
|
||||||
|
}
|
||||||
|
$tags = $tagsQuery->groupBy('tags.name')
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('user.profile')->with(compact('user', 'ejaculations', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stats($name)
|
public function stats($name)
|
||||||
@@ -49,7 +64,8 @@ SQL
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$groupByDay = Ejaculation::select(DB::raw(<<<'SQL'
|
$groupByDay = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
||||||
count(*) AS "count"
|
count(*) AS "count"
|
||||||
SQL
|
SQL
|
||||||
@@ -59,9 +75,22 @@ SQL
|
|||||||
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$groupByHour = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
|
to_char(ejaculated_date, 'HH24') AS "hour",
|
||||||
|
count(*) AS "count"
|
||||||
|
SQL
|
||||||
|
))
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
|
||||||
|
->orderBy(DB::raw('1'))
|
||||||
|
->get();
|
||||||
|
|
||||||
$dailySum = [];
|
$dailySum = [];
|
||||||
$monthlySum = [];
|
$monthlySum = [];
|
||||||
$yearlySum = [];
|
$yearlySum = [];
|
||||||
|
$dowSum = array_fill(0, 7, 0);
|
||||||
|
$hourlySum = array_fill(0, 24, 0);
|
||||||
|
|
||||||
// 年間グラフ用の配列初期化
|
// 年間グラフ用の配列初期化
|
||||||
if ($groupByDay->first() !== null) {
|
if ($groupByDay->first() !== null) {
|
||||||
@@ -73,7 +102,7 @@ SQL
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 月間グラフ用の配列初期化
|
// 月間グラフ用の配列初期化
|
||||||
$month = Carbon::now()->subMonth(11)->firstOfMonth(); // 直近12ヶ月
|
$month = Carbon::now()->firstOfMonth()->subMonth(11); // 直近12ヶ月
|
||||||
for ($i = 0; $i < 12; $i++) {
|
for ($i = 0; $i < 12; $i++) {
|
||||||
$monthlySum[$month->format('Y/m')] = 0;
|
$monthlySum[$month->format('Y/m')] = 0;
|
||||||
$month->addMonth();
|
$month->addMonth();
|
||||||
@@ -85,12 +114,18 @@ SQL
|
|||||||
|
|
||||||
$dailySum[$date->timestamp] = $data->count;
|
$dailySum[$date->timestamp] = $data->count;
|
||||||
$yearlySum[$date->year] += $data->count;
|
$yearlySum[$date->year] += $data->count;
|
||||||
|
$dowSum[$date->dayOfWeek] += $data->count;
|
||||||
if (isset($monthlySum[$yearAndMonth])) {
|
if (isset($monthlySum[$yearAndMonth])) {
|
||||||
$monthlySum[$yearAndMonth] += $data->count;
|
$monthlySum[$yearAndMonth] += $data->count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum'));
|
foreach ($groupByHour as $data) {
|
||||||
|
$hour = (int)$data->hour;
|
||||||
|
$hourlySum[$hour] += $data->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum', 'dowSum', 'hourlySum'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function okazu($name)
|
public function okazu($name)
|
||||||
@@ -101,7 +136,8 @@ SQL
|
|||||||
}
|
}
|
||||||
|
|
||||||
// チェックインの取得
|
// チェックインの取得
|
||||||
$query = Ejaculation::select(DB::raw(<<<'SQL'
|
$query = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
id,
|
id,
|
||||||
ejaculated_date,
|
ejaculated_date,
|
||||||
note,
|
note,
|
||||||
|
@@ -18,7 +18,7 @@ class RedirectIfAuthenticated
|
|||||||
public function handle($request, Closure $next, $guard = null)
|
public function handle($request, Closure $next, $guard = null)
|
||||||
{
|
{
|
||||||
if (Auth::guard($guard)->check()) {
|
if (Auth::guard($guard)->check()) {
|
||||||
return redirect('/home');
|
return redirect()->route('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
@@ -7,7 +7,7 @@ use Carbon\Carbon;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ProfileComposer
|
class ProfileStatsComposer
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -58,4 +58,4 @@ SQL
|
|||||||
|
|
||||||
$view->with(compact('latestEjaculation', 'currentSession', 'summary'));
|
$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;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Parsedown;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -13,7 +16,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
//
|
Blade::directive('parsedown', function ($expression) {
|
||||||
|
return "<?php echo app('parsedown')->text($expression); ?>";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +28,11 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
//
|
$this->app->singleton(MetadataResolver::class, function ($app) {
|
||||||
|
return new MetadataResolver();
|
||||||
|
});
|
||||||
|
$this->app->singleton('parsedown', function () {
|
||||||
|
return Parsedown::instance();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
|
||||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
class AuthServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class BroadcastServiceProvider extends ServiceProvider
|
class BroadcastServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
class EventServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -13,9 +13,9 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $listen = [
|
protected $listen = [
|
||||||
'App\Events\Event' => [
|
'App\Events\LinkDiscovered' => [
|
||||||
'App\Listeners\EventListener',
|
'App\Listeners\LinkCollector'
|
||||||
],
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
class RouteServiceProvider extends ServiceProvider
|
class RouteServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Http\ViewComposers\ProfileComposer;
|
use App\Http\ViewComposers\ProfileStatsComposer;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class ViewComposerServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
View::composer('components.profile', ProfileComposer::class);
|
View::composer('components.profile-stats', ProfileStatsComposer::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Illuminate\Notifications\Notifiable;
|
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
@@ -36,9 +36,10 @@ class User extends Authenticatable
|
|||||||
* @param int $size 画像サイズ
|
* @param int $size 画像サイズ
|
||||||
* @return string Gravatar 画像URL
|
* @return string Gravatar 画像URL
|
||||||
*/
|
*/
|
||||||
public function getProfileImageUrl($size = 30) : string
|
public function getProfileImageUrl($size = 30): string
|
||||||
{
|
{
|
||||||
$hash = md5(strtolower(trim($this->email)));
|
$hash = md5(strtolower(trim($this->email)));
|
||||||
|
|
||||||
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size;
|
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,6 +24,7 @@ class Formatter
|
|||||||
$days = floor($value / 86400);
|
$days = floor($value / 86400);
|
||||||
$hours = floor($value % 86400 / 3600);
|
$hours = floor($value % 86400 / 3600);
|
||||||
$minutes = floor($value % 3600 / 60);
|
$minutes = floor($value % 3600 / 60);
|
||||||
|
|
||||||
return "{$days}日 {$hours}時間 {$minutes}分";
|
return "{$days}日 {$hours}時間 {$minutes}分";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,4 +37,42 @@ class Formatter
|
|||||||
{
|
{
|
||||||
return $this->linkify->processUrls($text);
|
return $this->linkify->processUrls($text);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* URLを正規化します。
|
||||||
|
* @param string $url URL
|
||||||
|
* @return string 正規化されたURL
|
||||||
|
*/
|
||||||
|
public function normalizeUrl($url)
|
||||||
|
{
|
||||||
|
// Decode
|
||||||
|
$url = urldecode($url);
|
||||||
|
|
||||||
|
// Remove Hashbang
|
||||||
|
$url = preg_replace('~/#!/~u', '/', $url);
|
||||||
|
|
||||||
|
// Sort query parameters
|
||||||
|
$parts = parse_url($url);
|
||||||
|
if (!empty($parts['query'])) {
|
||||||
|
// Remove query parameters
|
||||||
|
$url = str_replace_last('?' . $parts['query'], '', $url);
|
||||||
|
if (!empty($parts['fragment'])) {
|
||||||
|
// Remove fragment identifier
|
||||||
|
$url = str_replace_last('#' . $parts['fragment'], '', $url);
|
||||||
|
} else {
|
||||||
|
// "http://example.com/?query#" の場合 $parts['fragment'] は unset になるので、個別に判定して除去する必要がある
|
||||||
|
$url = preg_replace('/#\z/u', '', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str($parts['query'], $params);
|
||||||
|
ksort($params);
|
||||||
|
|
||||||
|
$url = $url . '?' . http_build_query($params);
|
||||||
|
if (!empty($parts['fragment'])) {
|
||||||
|
$url .= '#' . $parts['fragment'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -5,19 +5,24 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0.0",
|
"php": ">=7.1.0",
|
||||||
|
"anhskohbo/no-captcha": "^3.0",
|
||||||
|
"doctrine/dbal": "^2.9",
|
||||||
|
"fideloper/proxy": "~3.3",
|
||||||
"guzzlehttp/guzzle": "^6.3",
|
"guzzlehttp/guzzle": "^6.3",
|
||||||
"laravel/framework": "5.5.*",
|
"laravel/framework": "5.5.*",
|
||||||
"laravel/tinker": "~1.0",
|
"laravel/tinker": "~1.0",
|
||||||
"misd/linkify": "^1.1",
|
"misd/linkify": "^1.1"
|
||||||
"parsedown/laravel": "~1.0"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.1",
|
"barryvdh/laravel-debugbar": "^3.1",
|
||||||
|
"barryvdh/laravel-ide-helper": "^2.5",
|
||||||
"filp/whoops": "~2.0",
|
"filp/whoops": "~2.0",
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.14",
|
||||||
"fzaninotto/faker": "~1.4",
|
"fzaninotto/faker": "~1.4",
|
||||||
"mockery/mockery": "~1.0",
|
"mockery/mockery": "~1.0",
|
||||||
"phpunit/phpunit": "~6.0"
|
"phpunit/phpunit": "~6.0",
|
||||||
|
"symfony/thanks": "^1.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
@@ -42,6 +47,9 @@
|
|||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
"@php artisan package:discover"
|
"@php artisan package:discover"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"php-cs-fixer fix"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
2603
composer.lock
generated
2603
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateUsersTable extends Migration
|
class CreateUsersTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreatePasswordResetsTable extends Migration
|
class CreatePasswordResetsTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateEjaculationsTable extends Migration
|
class CreateEjaculationsTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateInformationTable extends Migration
|
class CreateInformationTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class AddLinkToEjaculations extends Migration
|
class AddLinkToEjaculations extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateTagsTable extends Migration
|
class CreateTagsTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateMetadataTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('metadata', function (Blueprint $table) {
|
||||||
|
$table->string('url');
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('description');
|
||||||
|
$table->string('image');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class RecreateMetadataTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
Schema::create('metadata', function (Blueprint $table) {
|
||||||
|
$table->text('url');
|
||||||
|
$table->text('title');
|
||||||
|
$table->text('description');
|
||||||
|
$table->text('image');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class ChangeLinkOnEjaculations extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('ejaculations', function (Blueprint $table) {
|
||||||
|
$table->text('link')->default('')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('ejaculations', function (Blueprint $table) {
|
||||||
|
$table->string('link')->default('')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddExpiresOnMetadata extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('metadata', function (Blueprint $table) {
|
||||||
|
$table->timestamp('expires_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('metadata', function (Blueprint $table) {
|
||||||
|
$table->removeColumn('expires_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddBioAndUrlToUsers extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('bio', 160)->default('');
|
||||||
|
$table->text('url')->default('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('bio');
|
||||||
|
$table->dropColumn('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
8
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 */
|
/*# 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;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tis-need-agecheck .container {
|
||||||
|
filter: blur(45px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
transition: filter .15s liner;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item.no-side-border {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item.border-bottom-only:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
.list-group-item.border-bottom-only {
|
.list-group-item.border-bottom-only {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
@@ -27,4 +46,24 @@
|
|||||||
|
|
||||||
.timeline-action-item {
|
.timeline-action-item {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tis-global-count-graph {
|
||||||
|
height: 90px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, .125);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tis-page-selector {
|
||||||
|
margin-left: -1px;
|
||||||
|
width: calc(100% + 2px);
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.tis-sidebar-info {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
}
|
}
|
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' => [
|
'attribute-name' => [
|
||||||
'rule-name' => 'custom-message',
|
'rule-name' => 'custom-message',
|
||||||
],
|
],
|
||||||
|
'g-recaptcha-response' => [
|
||||||
|
'required' => '「私はロボットではありません」にチェックを入れてください。',
|
||||||
|
'captcha' => 'reCAPTCHAチェックに失敗しました。何度試しても解決しない場合、管理者にお問い合わせください。',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -117,4 +121,4 @@ return [
|
|||||||
'password' => 'パスワード',
|
'password' => 'パスワード',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', 'ログイン')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>ログイン</h2>
|
<h2>ログイン</h2>
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
||||||
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required autofocus>
|
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required autofocus>
|
||||||
|
|
||||||
@if ($errors->has('email'))
|
@if ($errors->has('email'))
|
||||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||||
@@ -25,12 +27,9 @@
|
|||||||
<div class="invalid-feedback">{{ $errors->first('password') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('password') }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="custom-control custom-checkbox mb-3">
|
||||||
<label class="custom-control custom-checkbox">
|
<input id="remember" name="remember" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
|
||||||
<input id="remember" name="rememver" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
|
<label class="custom-control-label" for="remember">保存する</label>
|
||||||
<span class="custom-control-indicator"></span>
|
|
||||||
<span class="custom-control-description">保存する</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" type="submit">ログイン</button>
|
<button class="btn btn-primary" type="submit">ログイン</button>
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', 'パスワードの再発行')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>パスワードの再発行</h2>
|
<h2>パスワードの再発行</h2>
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
||||||
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required autofocus>
|
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required autofocus>
|
||||||
|
|
||||||
@if ($errors->has('email'))
|
@if ($errors->has('email'))
|
||||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', 'パスワードの再発行')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>パスワードの再発行</h2>
|
<h2>パスワードの再発行</h2>
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
||||||
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required>
|
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required>
|
||||||
|
|
||||||
@if ($errors->has('email'))
|
@if ($errors->has('email'))
|
||||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||||
|
@@ -1,9 +1,21 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', '新規登録')
|
||||||
|
|
||||||
|
@push('head')
|
||||||
|
@if (!empty(config('captcha.secret')))
|
||||||
|
{!! NoCaptcha::renderJs() !!}
|
||||||
|
@endif
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>新規登録</h2>
|
<h2>新規登録</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<p class="mb-0"><strong>注意!</strong> Tissueでは、登録に使用したメールアドレスの <a href="https://ja.gravatar.com/" rel="noreferrer">Gravatar</a> を使用します。</p>
|
||||||
|
<p class="mb-0">他の場所での活動と紐付いてほしくない場合、使用予定のメールアドレスにGravatarが設定されていないかを確認することを推奨します。</p>
|
||||||
|
</div>
|
||||||
<div class="row justify-content-center my-5">
|
<div class="row justify-content-center my-5">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<form method="post" action="{{ route('register') }}">
|
<form method="post" action="{{ route('register') }}">
|
||||||
@@ -19,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
|
||||||
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required>
|
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required>
|
||||||
|
|
||||||
@if ($errors->has('email'))
|
@if ($errors->has('email'))
|
||||||
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
|
||||||
@@ -41,22 +53,26 @@
|
|||||||
<h6 class="mb-3">プライバシーに関するオプション (全て任意です)</h6>
|
<h6 class="mb-3">プライバシーに関するオプション (全て任意です)</h6>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="custom-control custom-checkbox mb-2">
|
||||||
<label class="custom-control custom-checkbox">
|
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
|
||||||
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
|
<label class="custom-control-label" for="protected">全てのチェックイン履歴を非公開にする</label>
|
||||||
<span class="custom-control-indicator"></span>
|
|
||||||
<span class="custom-control-description">全てのチェックイン履歴を非公開にする</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="custom-control custom-checkbox">
|
||||||
<label class="custom-control custom-checkbox">
|
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
|
||||||
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
|
<label class="custom-control-label" for="accept-analytics">匿名での統計にチェックインデータを利用することに同意します</label>
|
||||||
<span class="custom-control-indicator"></span>
|
|
||||||
<span class="custom-control-description">匿名での統計にチェックインデータを利用することに同意します</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (!empty(config('captcha.secret')))
|
||||||
|
<div class="form-row ml-1 mt-2 my-4">
|
||||||
|
<div class="mx-auto">
|
||||||
|
{!! NoCaptcha::display() !!}
|
||||||
|
</div>
|
||||||
|
@if ($errors->has('g-recaptcha-response'))
|
||||||
|
<div class="invalid-feedback d-block text-center">{{ $errors->first('g-recaptcha-response') }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button class="btn btn-primary btn-lg" type="submit">登録</button>
|
<button class="btn btn-primary btn-lg" type="submit">登録</button>
|
||||||
|
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 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<img src="{{ $user->getProfileImageUrl(64) }}" class="rounded mb-1">
|
<img src="{{ $user->getProfileImageUrl(128) }}" class="rounded mb-1">
|
||||||
<h4 class="card-title">
|
<h4 class="card-title">
|
||||||
<a class="text-dark" href="{{ route('user.profile', ['name' => $user->name]) }}">{{ $user->display_name }}</a>
|
<a class="text-dark" href="{{ route('user.profile', ['name' => $user->name]) }}">{{ $user->display_name }}</a>
|
||||||
</h4>
|
</h4>
|
||||||
@@ -11,22 +11,28 @@
|
|||||||
@endif
|
@endif
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
@if (!$user->is_protected)
|
{{-- Bio --}}
|
||||||
<h6 class="font-weight-bold mt-4"><span class="oi oi-timer"></span> 現在のセッション</h6>
|
@if (!empty($user->bio))
|
||||||
@if (isset($currentSession))
|
<p class="card-text mt-3 mb-0">
|
||||||
<p class="card-text mb-0">{{ $currentSession }}経過</p>
|
{!! Formatter::linkify(nl2br(e($user->bio))) !!}
|
||||||
<p class="card-text">({{ $latestEjaculation->ejaculated_date->format('Y/m/d H:i') }} にリセット)</p>
|
</p>
|
||||||
@else
|
@endif
|
||||||
<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>
|
{{-- URL --}}
|
||||||
<p class="card-text mb-0">平均記録: {{ Formatter::formatInterval($summary[0]->average) }}</p>
|
@if (!empty($user->url))
|
||||||
<p class="card-text mb-0">最長記録: {{ Formatter::formatInterval($summary[0]->longest) }}</p>
|
<p class="card-text d-flex mt-3">
|
||||||
<p class="card-text mb-0">最短記録: {{ Formatter::formatInterval($summary[0]->shortest) }}</p>
|
<span class="oi oi-link-intact mr-1 mt-1"></span>
|
||||||
<p class="card-text mb-0">合計時間: {{ Formatter::formatInterval($summary[0]->total_times) }}</p>
|
<a href="{{ $user->url }}" rel="me nofollow noopener" target="_blank" class="text-truncate">{{ preg_replace('~\Ahttps?://~', '', $user->url) }}</a>
|
||||||
<p class="card-text">通算回数: {{ $summary[0]->total_checkins }}回</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (!$user->is_protected || $user->isMe())
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
@component('components.profile-stats', ['user' => $user])
|
||||||
|
@endcomponent
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', 'チェックイン')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>今致してる?</h2>
|
<h2>今致してる?</h2>
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
<label for="date"><span class="oi oi-calendar"></span> 日付</label>
|
<label for="date"><span class="oi oi-calendar"></span> 日付</label>
|
||||||
<input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
<input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
||||||
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? date('Y/m/d') }}" required>
|
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? $defaults['date'] }}" required>
|
||||||
|
|
||||||
@if ($errors->has('date'))
|
@if ($errors->has('date'))
|
||||||
<div class="invalid-feedback">{{ $errors->first('date') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('date') }}</div>
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
<label for="time"><span class="oi oi-clock"></span> 時刻</label>
|
<label for="time"><span class="oi oi-clock"></span> 時刻</label>
|
||||||
<input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
<input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
||||||
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? date('H:i') }}" required>
|
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? $defaults['time'] }}" required>
|
||||||
|
|
||||||
@if ($errors->has('time'))
|
@if ($errors->has('time'))
|
||||||
<div class="invalid-feedback">{{ $errors->first('time') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('time') }}</div>
|
||||||
@@ -36,9 +38,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<input name="tags" type="hidden" value="{{ old('tags') }}">
|
<input name="tags" type="hidden" value="{{ old('tags') ?? $defaults['tags'] }}">
|
||||||
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
|
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
|
||||||
<div class="form-control {{ $errors->has('tags') ? ' is-invalid' : '' }}">
|
<div class="form-control h-auto {{ $errors->has('tags') ? ' is-invalid' : '' }}">
|
||||||
<ul id="tags" class="list-inline d-inline"></ul>
|
<ul id="tags" class="list-inline d-inline"></ul>
|
||||||
<input id="tagInput" type="text" style="outline: 0; border: 0;">
|
<input id="tagInput" type="text" style="outline: 0; border: 0;">
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +56,7 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
|
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
|
||||||
<input id="link" name="link" type="text" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') }}">
|
<input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $defaults['link'] }}">
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
オカズのURLを貼り付けて登録することができます。
|
オカズのURLを貼り付けて登録することができます。
|
||||||
</small>
|
</small>
|
||||||
@@ -62,11 +64,13 @@
|
|||||||
<div class="invalid-feedback">{{ $errors->first('link') }}</div>
|
<div class="invalid-feedback">{{ $errors->first('link') }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@component('components.card', ['link' => null])
|
||||||
|
@endcomponent
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
|
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
|
||||||
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') }}</textarea>
|
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $defaults['note'] }}</textarea>
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
最大 500 文字
|
最大 500 文字
|
||||||
</small>
|
</small>
|
||||||
@@ -78,13 +82,10 @@
|
|||||||
<div class="form-row mt-4">
|
<div class="form-row mt-4">
|
||||||
<p>オプション</p>
|
<p>オプション</p>
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<div class="form-check">
|
<div class="custom-control custom-checkbox mb-3">
|
||||||
<label class="custom-control custom-checkbox">
|
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') || $defaults['is_private'] ? 'checked' : '' }}>
|
||||||
<input name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') ? 'checked' : '' }}>
|
<label class="custom-control-label" for="isPrivate">
|
||||||
<span class="custom-control-indicator"></span>
|
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||||
<span class="custom-control-description">
|
|
||||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,4 +151,60 @@
|
|||||||
$('#tagInput').focus();
|
$('#tagInput').focus();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
function() { document.getElementById("link").onchange = linkChanged;},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
function linkChanged(event) {
|
||||||
|
const card = document.querySelector(".link-card");
|
||||||
|
const url = event.target.value;
|
||||||
|
hiddenCard(card);
|
||||||
|
updateCard(card, url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function updateCard(card, url) {
|
||||||
|
const data = await getCardInfo(url);
|
||||||
|
card.querySelector(".card-title").innerHTML = data.title;
|
||||||
|
card.querySelector(".card-text").innerHTML = data.description;
|
||||||
|
if(data.image){
|
||||||
|
card.querySelector("img").src = data.image;
|
||||||
|
}else {
|
||||||
|
card.querySelector("img").classList.add("d-none");
|
||||||
|
}
|
||||||
|
card.classList.remove('col-md-6');
|
||||||
|
|
||||||
|
showCard(card);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function getCardInfo(url) {
|
||||||
|
return $.ajax({
|
||||||
|
url: '/api/checkin/card',
|
||||||
|
method: 'get',
|
||||||
|
type: 'json',
|
||||||
|
data: {
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
}).then(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
}).catch(function(e) {
|
||||||
|
console.log(e); // "oh, no!"
|
||||||
|
return {
|
||||||
|
title: "Error",
|
||||||
|
description: "Error",
|
||||||
|
image: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function showCard(card) {
|
||||||
|
card.classList.remove("d-none");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function hiddenCard(card) {
|
||||||
|
card.classList.add("d-none");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@endpush
|
@endpush
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', 'チェックインの修正')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>チェックインの修正</h2>
|
<h2>チェックインの修正</h2>
|
||||||
@@ -37,9 +39,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<input name="tags" type="hidden" value="{{ old('tags') ?? implode(' ', $ejaculation->tags->map(function ($v) { return $v->name; })->all()) }}">
|
<input name="tags" type="hidden" value="{{ old('tags') ?? $ejaculation->textTags() }}">
|
||||||
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
|
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
|
||||||
<div class="form-control {{ $errors->has('tags') ? ' is-invalid' : '' }}">
|
<div class="form-control h-auto {{ $errors->has('tags') ? ' is-invalid' : '' }}">
|
||||||
<ul id="tags" class="list-inline d-inline"></ul>
|
<ul id="tags" class="list-inline d-inline"></ul>
|
||||||
<input id="tagInput" type="text" style="outline: 0; border: 0;">
|
<input id="tagInput" type="text" style="outline: 0; border: 0;">
|
||||||
</div>
|
</div>
|
||||||
@@ -52,21 +54,10 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{--
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group col-sm-12">
|
|
||||||
<label for="tags"><span class="oi oi-tags"></span> タグ</label>
|
|
||||||
<input id="tags" type="text" class="form-control" placeholder="未実装です" disabled>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
スペース区切りで複数入力できます。
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
--}}
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
|
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
|
||||||
<input id="link" name="link" type="text" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}">
|
<input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}">
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
オカズのURLを貼り付けて登録することができます。
|
オカズのURLを貼り付けて登録することができます。
|
||||||
</small>
|
</small>
|
||||||
@@ -90,13 +81,10 @@
|
|||||||
<div class="form-row mt-4">
|
<div class="form-row mt-4">
|
||||||
<p>オプション</p>
|
<p>オプション</p>
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<div class="form-check">
|
<div class="custom-control custom-checkbox mb-3">
|
||||||
<label class="custom-control custom-checkbox">
|
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
|
||||||
<input name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
|
<label class="custom-control-label" for="isPrivate">
|
||||||
<span class="custom-control-indicator"></span>
|
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||||
<span class="custom-control-description">
|
|
||||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@if (!$user->isMe() && ($user->is_protected || $ejaculation->is_private))
|
||||||
|
@section('title', $user->display_name . ' さんのチェックイン')
|
||||||
|
@else
|
||||||
|
@section('title', $user->display_name . ' さんのチェックイン (' . $ejaculation->ejaculated_date->format('n月j日 H:i') . ')')
|
||||||
|
@endif
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -26,12 +32,13 @@
|
|||||||
<!-- span -->
|
<!-- span -->
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
|
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
|
||||||
@if ($user->isMe())
|
<div>
|
||||||
<div>
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
|
||||||
|
@if ($user->isMe())
|
||||||
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
|
||||||
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
|
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
|
||||||
</div>
|
@endif
|
||||||
@endif
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- tags -->
|
<!-- tags -->
|
||||||
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
||||||
@@ -40,24 +47,19 @@
|
|||||||
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
|
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
|
||||||
@endif
|
@endif
|
||||||
@foreach ($ejaculation->tags as $tag)
|
@foreach ($ejaculation->tags as $tag)
|
||||||
<span class="badge badge-secondary"><span class="oi oi-tag"></span> {{ $tag->name }}</span>
|
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
<!-- okazu link -->
|
<!-- okazu link -->
|
||||||
@if (!empty($ejaculation->link))
|
@if (!empty($ejaculation->link))
|
||||||
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;">
|
<div class="row mx-0">
|
||||||
<a class="text-dark card-link" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">
|
@component('components.card', ['link' => $ejaculation->link])
|
||||||
<img src="" alt="Thumbnail" class="card-img-top bg-secondary">
|
@endcomponent
|
||||||
<div class="card-body">
|
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
|
||||||
<h6 class="card-title font-weight-bold">タイトル</h6>
|
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
|
||||||
<p class="card-text">コンテンツの説明文</p>
|
</p>
|
||||||
</div>
|
</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
|
@endif
|
||||||
<!-- note -->
|
<!-- note -->
|
||||||
@if (!empty($ejaculation->note))
|
@if (!empty($ejaculation->note))
|
||||||
@@ -103,42 +105,8 @@
|
|||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.link-card').each(function () {
|
$('.link-card').linkCard({
|
||||||
var $this = $(this);
|
endpoint: '{{ url('/api/checkin/card') }}'
|
||||||
$.ajax({
|
|
||||||
url: '{{ url('/api/checkin/card') }}',
|
|
||||||
method: 'get',
|
|
||||||
type: 'json',
|
|
||||||
data: {
|
|
||||||
url: $this.find('a').attr('href')
|
|
||||||
}
|
|
||||||
}).then(function (data) {
|
|
||||||
var $title = $this.find('.card-title');
|
|
||||||
var $desc = $this.find('.card-text');
|
|
||||||
var $image = $this.find('img');
|
|
||||||
|
|
||||||
if (data.title === '') {
|
|
||||||
$title.hide();
|
|
||||||
} else {
|
|
||||||
$title.text(data.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.description === '') {
|
|
||||||
$desc.hide();
|
|
||||||
} else {
|
|
||||||
$desc.text(data.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.image === '') {
|
|
||||||
$image.hide();
|
|
||||||
} else {
|
|
||||||
$image.attr('src', data.image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.title !== '' || data.description !== '' || data.image !== '') {
|
|
||||||
$this.removeClass('d-none');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
@@ -31,6 +31,9 @@
|
|||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
@foreach($informations as $info)
|
@foreach($informations as $info)
|
||||||
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
|
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
|
||||||
|
@if ($info->pinned)
|
||||||
|
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
|
||||||
|
@endif
|
||||||
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
|
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
@@ -7,114 +7,135 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
@component('components.profile', ['user' => Auth::user()])
|
<div class="card mb-4">
|
||||||
@endcomponent
|
<div class="card-body">
|
||||||
</div>
|
<div class="d-flex flex-row align-items-end mb-4">
|
||||||
<div class="col-lg-8">
|
<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 mb-4">
|
||||||
<div class="card-header">サイトからのお知らせ</div>
|
<div class="card-header">サイトからのお知らせ</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush tis-sidebar-info">
|
||||||
@foreach($informations as $info)
|
@foreach($informations as $info)
|
||||||
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
|
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
|
||||||
|
@if ($info->pinned)
|
||||||
|
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
|
||||||
|
@endif
|
||||||
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
|
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
<a href="{{ route('info') }}" class="list-group-item text-right">お知らせ一覧 »</a>
|
<a href="{{ route('info') }}" class="list-group-item text-right">お知らせ一覧 »</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (!empty($publicLinkedEjaculations))
|
</div>
|
||||||
<div class="card mb-4">
|
<div class="col-lg-8">
|
||||||
<div class="card-header">お惣菜コーナー</div>
|
@if (!empty($globalEjaculationCounts))
|
||||||
<div class="card-body">
|
<h5>チェックインの動向</h5>
|
||||||
<p class="card-text">最近の公開チェックインから、オカズリンク付きのものを表示しています。</p>
|
<div class="w-100 mb-4 position-relative tis-global-count-graph">
|
||||||
</div>
|
<canvas id="global-count-graph"></canvas>
|
||||||
<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>
|
||||||
@endif
|
@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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('script')
|
@push('script')
|
||||||
|
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/chart.js@2.7.1/dist/Chart.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$('.link-card').each(function () {
|
$('.link-card').linkCard({
|
||||||
var $this = $(this);
|
endpoint: '{{ url('/api/checkin/card') }}'
|
||||||
$.ajax({
|
});
|
||||||
url: '{{ url('/api/checkin/card') }}',
|
|
||||||
method: 'get',
|
|
||||||
type: 'json',
|
|
||||||
data: {
|
|
||||||
url: $this.find('a').attr('href')
|
|
||||||
}
|
|
||||||
}).then(function (data) {
|
|
||||||
var $title = $this.find('.card-title');
|
|
||||||
var $desc = $this.find('.card-text');
|
|
||||||
var $image = $this.find('img');
|
|
||||||
|
|
||||||
if (data.title === '') {
|
new Chart(document.getElementById('global-count-graph').getContext('2d'), {
|
||||||
$title.hide();
|
type: 'bar',
|
||||||
} else {
|
data: {
|
||||||
$title.text(data.title);
|
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>
|
</script>
|
||||||
@endpush
|
@endpush
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', 'お知らせ')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>サイトからのお知らせ</h2>
|
<h2>サイトからのお知らせ</h2>
|
||||||
@@ -7,26 +9,13 @@
|
|||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
@foreach($informations as $info)
|
@foreach($informations as $info)
|
||||||
<a class="list-group-item border-bottom-only pt-3 pb-3" href="{{ route('info.show', ['id' => $info->id]) }}">
|
<a class="list-group-item border-bottom-only pt-3 pb-3" href="{{ route('info.show', ['id' => $info->id]) }}">
|
||||||
|
@if ($info->pinned)
|
||||||
|
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
|
||||||
|
@endif
|
||||||
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
|
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
<ul class="pagination mt-4 justify-content-center">
|
{{ $informations->links(null, ['className' => 'mt-4 justify-content-center']) }}
|
||||||
<li class="page-item {{ $informations->currentPage() === 1 ? 'disabled' : '' }}">
|
|
||||||
<a class="page-link" href="{{ $informations->previousPageUrl() }}" aria-label="Previous">
|
|
||||||
<span aria-hidden="true">«</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>
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
@@ -1,5 +1,7 @@
|
|||||||
@extends('layouts.base')
|
@extends('layouts.base')
|
||||||
|
|
||||||
|
@section('title', $category['label'] . ': ' . $info->title)
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav aria-label="breadcrumb" role="navigation">
|
<nav aria-label="breadcrumb" role="navigation">
|
||||||
@@ -9,7 +11,12 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<h2><span class="badge {{ $category['class'] }}">{{ $category['label'] }}</span> {{ $info->title }}</h2>
|
<h2><span class="badge {{ $category['class'] }}">{{ $category['label'] }}</span> {{ $info->title }}</h2>
|
||||||
<p class="text-secondary"><span class="oi oi-calendar"></span> {{ $info->created_at->format('Y年n月j日') }}</p>
|
<p class="text-secondary">
|
||||||
|
@if ($info->pinned)
|
||||||
|
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
|
||||||
|
@endif
|
||||||
|
<span class="oi oi-calendar"></span> {{ $info->created_at->format('Y年n月j日') }}
|
||||||
|
</p>
|
||||||
@parsedown($info->content)
|
@parsedown($info->content)
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
@@ -6,7 +6,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<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/bootstrap.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ asset('css/open-iconic-bootstrap.min.css') }}" rel="stylesheet">
|
<link href="{{ asset('css/open-iconic-bootstrap.min.css') }}" rel="stylesheet">
|
||||||
@@ -14,7 +18,18 @@
|
|||||||
|
|
||||||
@stack('head')
|
@stack('head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="{{Auth::check() ? '' : 'tis-need-agecheck'}}">
|
||||||
|
<noscript class="navbar navbar-light bg-warning">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex flex-column mx-auto">
|
||||||
|
<p class="m-0 text-dark">Tissueを利用するには、ブラウザのJavaScriptとCookieを有効にする必要があります。</p>
|
||||||
|
<p class="m-0 text-info">
|
||||||
|
<a href="https://www.enable-javascript.com/ja/" target="_blank" rel="nofollow noopener">ブラウザでJavaScriptを有効にする方法</a>
|
||||||
|
・ <a href="https://www.whatismybrowser.com/guides/how-to-enable-cookies/auto" target="_blank" rel="nofollow noopener">ブラウザでCookieを有効にする方法</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light {{ !Auth::check() && Route::currentRouteName() === 'home' ? '' : 'mb-4'}}">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light {{ !Auth::check() && Route::currentRouteName() === 'home' ? '' : 'mb-4'}}">
|
||||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
|
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
@@ -44,24 +59,43 @@
|
|||||||
<a class="nav-link" href="{{ route('ranking') }}">ランキング</a>
|
<a class="nav-link" href="{{ route('ranking') }}">ランキング</a>
|
||||||
</li>--}}
|
</li>--}}
|
||||||
</ul>
|
</ul>
|
||||||
|
<form action="{{ stripos(Route::currentRouteName(), 'search') === 0 ? route(Route::currentRouteName()) : route('search') }}" class="form-inline mr-2">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="search" name="q" class="form-control" placeholder="検索..." value="{{ stripos(Route::currentRouteName(), 'search') === 0 ? $inputs['q'] : '' }}" required>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit"><span class="oi oi-magnifying-glass" aria-hidden="true"></span><span class="sr-only">検索</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form class="form-inline mr-2">
|
||||||
|
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
|
||||||
|
</form>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top mr-2">
|
<img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top">
|
||||||
{{ Auth::user()->display_name }} さん
|
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
|
||||||
{{--<a href="#" class="dropdown-item">設定</a>--}}
|
<a href="{{ route('user.profile', ['name' => Auth::user()->name]) }}" class="dropdown-item">
|
||||||
|
<strong>{{ Auth::user()->display_name }}</strong>
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
<span>@{{ Auth::user()->name }}</span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
|
||||||
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
|
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="form-inline">
|
|
||||||
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
|
|
||||||
</form>
|
|
||||||
@endauth
|
@endauth
|
||||||
@guest
|
@guest
|
||||||
<form class="form-inline ml-auto">
|
<ul class="navbar-nav ml-auto mr-2">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('register') }}" class="nav-link">会員登録</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form class="form-inline">
|
||||||
<a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a>
|
<a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a>
|
||||||
</form>
|
</form>
|
||||||
@endguest
|
@endguest
|
||||||
@@ -81,21 +115,52 @@
|
|||||||
@yield('content')
|
@yield('content')
|
||||||
<footer class="tis-footer mt-4">
|
<footer class="tis-footer mt-4">
|
||||||
<div class="container p-3 p-md-4">
|
<div class="container p-3 p-md-4">
|
||||||
<p>Copyright (c) 2017 shikorism.net</p>
|
<p>Copyright (c) 2017-2019 shikorism.net</p>
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
<li class="list-inline-item"><a href="https://github.com/shibafu528" class="text-dark">Admin(@shibafu528)</a></li>
|
<li class="list-inline-item"><a href="https://github.com/shibafu528" class="text-dark">Admin(@shibafu528)</a></li>
|
||||||
<li class="list-inline-item"><a href="https://github.com/shikorism/tissue" class="text-dark">GitHub</a></li>
|
<li class="list-inline-item"><a href="https://github.com/shikorism/tissue" class="text-dark">GitHub</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@guest
|
||||||
|
<div class="modal fade" id="ageCheckModal" tabindex="-1" role="dialog" aria-labelledby="ageCheckModalTitle" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="ageCheckModalTitle">Tissue へようこそ!</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
この先のコンテンツには暴力表現や性描写など、18歳未満の方が閲覧できないコンテンツが含まれています。
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-dismiss="modal">まかせて</button>
|
||||||
|
<a href="https://cookpad.com" rel="noreferrer" class="btn btn-secondary">ごめん無理</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endguest
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.js"></script>
|
||||||
<script type="text/javascript" src="{{ asset('js/bootstrap.min.js') }}"></script>
|
<script type="text/javascript" src="{{ asset('js/bootstrap.min.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ asset('js/tissue.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
@guest
|
||||||
|
if (Cookies.get('agechecked')) {
|
||||||
|
$('body').removeClass('tis-need-agecheck');
|
||||||
|
} else {
|
||||||
|
$('#ageCheckModal').modal({ backdrop: 'static' })
|
||||||
|
.on('hide.bs.modal', function() {
|
||||||
|
$('body').removeClass('tis-need-agecheck');
|
||||||
|
Cookies.set('agechecked', '1', { expires: 365 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@endguest
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
$('.alert').alert();
|
$('.alert').alert();
|
||||||
|
$('.tis-page-selector').pageSelector();
|
||||||
@if (session('status'))
|
@if (session('status'))
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$('#status').alert('close');
|
$('#status').alert('close');
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user