mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-22 21:13:16 +00:00
commit
14a1a0283c
73 changed files with 1667 additions and 1 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.4...dev)
|
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.4...dev)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705))
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
|
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
|
||||||
- Update PublicApiController, remove expensive and unused relationships ([2ecc3144](https://github.com/pixelfed/pixelfed/commit/2ecc3144))
|
- Update PublicApiController, remove expensive and unused relationships ([2ecc3144](https://github.com/pixelfed/pixelfed/commit/2ecc3144))
|
||||||
|
|
318
app/Http/Controllers/PortfolioController.php
Normal file
318
app/Http/Controllers/PortfolioController.php
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Portfolio;
|
||||||
|
use Cache;
|
||||||
|
use DB;
|
||||||
|
use App\Status;
|
||||||
|
use App\User;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
class PortfolioController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return view('portfolio.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, $username)
|
||||||
|
{
|
||||||
|
$user = User::whereUsername($username)->first();
|
||||||
|
|
||||||
|
if(!$user) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($user->id)->firstOrFail();
|
||||||
|
$user = AccountService::get($user->profile_id);
|
||||||
|
|
||||||
|
if($user['locked']) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($portfolio->active != true) {
|
||||||
|
if(!$request->user()) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->user()->profile_id == $user['id']) {
|
||||||
|
return redirect(config('portfolio.path') . '/settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('portfolio.show', compact('user', 'portfolio'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showPost(Request $request, $username, $id)
|
||||||
|
{
|
||||||
|
$authed = $request->user();
|
||||||
|
$post = StatusService::get($id);
|
||||||
|
|
||||||
|
if(!$post) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = AccountService::get($post['account']['id']);
|
||||||
|
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
||||||
|
|
||||||
|
if($user['locked'] || $portfolio->active != true) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$post || $post['visibility'] != 'public' || $post['pf_type'] != 'photo' || $user['id'] != $post['account']['id']) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('portfolio.show_post', compact('user', 'post', 'authed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function myRedirect(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 404);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if(Portfolio::whereProfileId($user->profile_id)->exists() === false) {
|
||||||
|
$portfolio = new Portfolio;
|
||||||
|
$portfolio->profile_id = $user->profile_id;
|
||||||
|
$portfolio->user_id = $user->id;
|
||||||
|
$portfolio->active = false;
|
||||||
|
$portfolio->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain = config('portfolio.domain');
|
||||||
|
$path = config('portfolio.path');
|
||||||
|
$url = 'https://' . $domain . $path;
|
||||||
|
|
||||||
|
return redirect($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function settings(Request $request)
|
||||||
|
{
|
||||||
|
if(!$request->user()) {
|
||||||
|
return redirect(route('home'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
||||||
|
|
||||||
|
if(!$portfolio) {
|
||||||
|
$portfolio = new Portfolio;
|
||||||
|
$portfolio->user_id = $request->user()->id;
|
||||||
|
$portfolio->profile_id = $request->user()->profile_id;
|
||||||
|
$portfolio->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('portfolio.settings', compact('portfolio'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
abort_unless($request->user(), 404);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'profile_source' => 'required|in:recent,custom',
|
||||||
|
'layout' => 'required|in:grid,masonry',
|
||||||
|
'layout_container' => 'required|in:fixed,fluid'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
||||||
|
|
||||||
|
if(!$portfolio) {
|
||||||
|
$portfolio = new Portfolio;
|
||||||
|
$portfolio->user_id = $request->user()->id;
|
||||||
|
$portfolio->profile_id = $request->user()->profile_id;
|
||||||
|
$portfolio->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio->active = $request->input('enabled') === 'on';
|
||||||
|
$portfolio->show_captions = $request->input('show_captions') === 'on';
|
||||||
|
$portfolio->show_license = $request->input('show_license') === 'on';
|
||||||
|
$portfolio->show_location = $request->input('show_location') === 'on';
|
||||||
|
$portfolio->show_timestamp = $request->input('show_timestamp') === 'on';
|
||||||
|
$portfolio->show_link = $request->input('show_link') === 'on';
|
||||||
|
$portfolio->profile_source = $request->input('profile_source');
|
||||||
|
$portfolio->show_avatar = $request->input('show_avatar') === 'on';
|
||||||
|
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
||||||
|
$portfolio->profile_layout = $request->input('layout');
|
||||||
|
$portfolio->profile_container = $request->input('layout_container');
|
||||||
|
$portfolio->save();
|
||||||
|
|
||||||
|
return redirect('/' . $request->user()->username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFeed(Request $request, $id)
|
||||||
|
{
|
||||||
|
$user = AccountService::get($id, true);
|
||||||
|
|
||||||
|
if(!$user || !isset($user['id'])) {
|
||||||
|
return response()->json([], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
||||||
|
|
||||||
|
if(!$portfolio || !$portfolio->active) {
|
||||||
|
return response()->json([], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($portfolio->profile_source === 'custom' && $portfolio->metadata) {
|
||||||
|
return $this->getCustomFeed($portfolio);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getRecentFeed($user['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCustomFeed($portfolio) {
|
||||||
|
if(!$portfolio->metadata['posts']) {
|
||||||
|
return response()->json([], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($portfolio->metadata['posts'])->map(function($p) {
|
||||||
|
return StatusService::get($p);
|
||||||
|
})
|
||||||
|
->filter(function($p) {
|
||||||
|
return $p && isset($p['account']);
|
||||||
|
})->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRecentFeed($id) {
|
||||||
|
$media = Cache::remember('portfolio:recent-feed:' . $id, 3600, function() use($id) {
|
||||||
|
return DB::table('media')
|
||||||
|
->whereProfileId($id)
|
||||||
|
->whereNotNull('status_id')
|
||||||
|
->groupBy('status_id')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->take(50)
|
||||||
|
->pluck('status_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
return $media->map(function($sid) use($id) {
|
||||||
|
return StatusService::get($sid);
|
||||||
|
})
|
||||||
|
->filter(function($post) {
|
||||||
|
return $post &&
|
||||||
|
isset($post['media_attachments']) &&
|
||||||
|
!empty($post['media_attachments']) &&
|
||||||
|
$post['pf_type'] === 'photo' &&
|
||||||
|
$post['visibility'] === 'public';
|
||||||
|
})
|
||||||
|
->take(24)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSettings(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$res = Portfolio::whereUserId($request->user()->id)->get();
|
||||||
|
|
||||||
|
if(!$res) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res->map(function($p) {
|
||||||
|
return [
|
||||||
|
'url' => $p->url(),
|
||||||
|
'pid' => (string) $p->profile_id,
|
||||||
|
'active' => (bool) $p->active,
|
||||||
|
'show_captions' => (bool) $p->show_captions,
|
||||||
|
'show_license' => (bool) $p->show_license,
|
||||||
|
'show_location' => (bool) $p->show_location,
|
||||||
|
'show_timestamp' => (bool) $p->show_timestamp,
|
||||||
|
'show_link' => (bool) $p->show_link,
|
||||||
|
'show_avatar' => (bool) $p->show_avatar,
|
||||||
|
'show_bio' => (bool) $p->show_bio,
|
||||||
|
'profile_layout' => $p->profile_layout,
|
||||||
|
'profile_source' => $p->profile_source,
|
||||||
|
'metadata' => $p->metadata
|
||||||
|
];
|
||||||
|
})->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccountSettings(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required|integer'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$account = AccountService::get($request->input('id'));
|
||||||
|
|
||||||
|
abort_if(!$account, 404);
|
||||||
|
|
||||||
|
$p = Portfolio::whereProfileId($request->input('id'))->whereActive(1)->firstOrFail();
|
||||||
|
|
||||||
|
if(!$p) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => $p->url(),
|
||||||
|
'show_captions' => (bool) $p->show_captions,
|
||||||
|
'show_license' => (bool) $p->show_license,
|
||||||
|
'show_location' => (bool) $p->show_location,
|
||||||
|
'show_timestamp' => (bool) $p->show_timestamp,
|
||||||
|
'show_link' => (bool) $p->show_link,
|
||||||
|
'show_avatar' => (bool) $p->show_avatar,
|
||||||
|
'show_bio' => (bool) $p->show_bio,
|
||||||
|
'profile_layout' => $p->profile_layout,
|
||||||
|
'profile_source' => $p->profile_source
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeSettings(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'profile_layout' => 'sometimes|in:grid,masonry,album'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$res = Portfolio::whereUserId($request->user()->id)
|
||||||
|
->update($request->only([
|
||||||
|
'active',
|
||||||
|
'show_captions',
|
||||||
|
'show_license',
|
||||||
|
'show_location',
|
||||||
|
'show_timestamp',
|
||||||
|
'show_link',
|
||||||
|
'show_avatar',
|
||||||
|
'show_bio',
|
||||||
|
'profile_layout',
|
||||||
|
'profile_source'
|
||||||
|
]));
|
||||||
|
|
||||||
|
Cache::forget('portfolio:recent-feed:' . $request->user()->profile_id);
|
||||||
|
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeCurated(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'ids' => 'required|array|max:24'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pid = $request->user()->profile_id;
|
||||||
|
|
||||||
|
$ids = $request->input('ids');
|
||||||
|
|
||||||
|
Status::whereProfileId($pid)
|
||||||
|
->whereScope('public')
|
||||||
|
->whereIn('type', ['photo', 'photo:album'])
|
||||||
|
->findOrFail($ids);
|
||||||
|
|
||||||
|
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
||||||
|
$p->metadata = ['posts' => $ids];
|
||||||
|
$p->save();
|
||||||
|
|
||||||
|
Cache::forget('portfolio:recent-feed:' . $pid);
|
||||||
|
|
||||||
|
return $request->ids;
|
||||||
|
}
|
||||||
|
}
|
39
app/Models/Portfolio.php
Normal file
39
app/Models/Portfolio.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
|
||||||
|
class Portfolio extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $fillable = [
|
||||||
|
'active',
|
||||||
|
'show_captions',
|
||||||
|
'show_license',
|
||||||
|
'show_location',
|
||||||
|
'show_timestamp',
|
||||||
|
'show_link',
|
||||||
|
'show_avatar',
|
||||||
|
'show_bio',
|
||||||
|
'profile_layout',
|
||||||
|
'profile_source'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'metadata' => 'json'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
$account = AccountService::get($this->profile_id);
|
||||||
|
if(!$account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'https://' . config('portfolio.domain') . config('portfolio.path') . '/' . $account['username'];
|
||||||
|
}
|
||||||
|
}
|
31
config/portfolio.php
Normal file
31
config/portfolio.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Portfolio Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the domain used for the portfolio feature. Only change
|
||||||
|
| the default value if you have a subdomain configured. You must use
|
||||||
|
| a subdomain on the same app domain.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'domain' => env('PORTFOLIO_DOMAIN', config('pixelfed.domain.app')),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Portfolio Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the path used for the portfolio feature. Only change
|
||||||
|
| the default value if you have a subdomain configured. If you want
|
||||||
|
| to use the root path of the subdomain, leave this value empty.
|
||||||
|
|
|
||||||
|
| WARNING: SETTING THIS VALUE WITHOUT A SUBDOMAIN COULD BREAK YOUR
|
||||||
|
| INSTANCE, SO ONLY CHANGE THIS IF YOU KNOW WHAT YOU'RE DOING.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'path' => env('PORTFOLIO_PATH', '/i/portfolio'),
|
||||||
|
];
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreatePortfoliosTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('portfolios', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedInteger('user_id')->nullable()->unique()->index();
|
||||||
|
$table->bigInteger('profile_id')->unsigned()->unique()->index();
|
||||||
|
$table->boolean('active')->nullable()->index();
|
||||||
|
$table->boolean('show_captions')->default(true)->nullable();
|
||||||
|
$table->boolean('show_license')->default(true)->nullable();
|
||||||
|
$table->boolean('show_location')->default(true)->nullable();
|
||||||
|
$table->boolean('show_timestamp')->default(true)->nullable();
|
||||||
|
$table->boolean('show_link')->default(true)->nullable();
|
||||||
|
$table->string('profile_source')->default('recent')->nullable();
|
||||||
|
$table->boolean('show_avatar')->default(true)->nullable();
|
||||||
|
$table->boolean('show_bio')->default(true)->nullable();
|
||||||
|
$table->string('profile_layout')->default('grid')->nullable();
|
||||||
|
$table->string('profile_container')->default('fixed')->nullable();
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('portfolios');
|
||||||
|
}
|
||||||
|
}
|
BIN
public/css/portfolio.css
vendored
Normal file
BIN
public/css/portfolio.css
vendored
Normal file
Binary file not shown.
BIN
public/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2
Normal file
BIN
public/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2
Normal file
Binary file not shown.
BIN
public/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2
Normal file
BIN
public/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2
Normal file
Binary file not shown.
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/collections.js
vendored
BIN
public/js/collections.js
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/compose-classic.js
vendored
BIN
public/js/compose-classic.js
vendored
Binary file not shown.
BIN
public/js/compose-ivl9d2teh.js
vendored
BIN
public/js/compose-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/compose-llsjbikoc.js
vendored
Normal file
BIN
public/js/compose-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
BIN
public/js/daci-ivl9d2teh.js
vendored
BIN
public/js/daci-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/daci-llsjbikoc.js
vendored
Normal file
BIN
public/js/daci-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/developers.js
vendored
BIN
public/js/developers.js
vendored
Binary file not shown.
BIN
public/js/dffc-ivl9d2teh.js
vendored
BIN
public/js/dffc-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dffc-llsjbikoc.js
vendored
Normal file
BIN
public/js/dffc-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/direct.js
vendored
BIN
public/js/direct.js
vendored
Binary file not shown.
BIN
public/js/discover-ivl9d2teh.js
vendored
BIN
public/js/discover-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/discover-llsjbikoc.js
vendored
Normal file
BIN
public/js/discover-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dms-ivl9d2teh.js
vendored
BIN
public/js/dms-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dms-llsjbikoc.js
vendored
Normal file
BIN
public/js/dms-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dmsg-ivl9d2teh.js
vendored
BIN
public/js/dmsg-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dmsg-llsjbikoc.js
vendored
Normal file
BIN
public/js/dmsg-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dmyh-ivl9d2teh.js
vendored
BIN
public/js/dmyh-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dmyh-llsjbikoc.js
vendored
Normal file
BIN
public/js/dmyh-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dmym-ivl9d2teh.js
vendored
BIN
public/js/dmym-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dmym-llsjbikoc.js
vendored
Normal file
BIN
public/js/dmym-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dsfc-ivl9d2teh.js
vendored
BIN
public/js/dsfc-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dsfc-llsjbikoc.js
vendored
Normal file
BIN
public/js/dsfc-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dssc-ivl9d2teh.js
vendored
BIN
public/js/dssc-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/dssc-llsjbikoc.js
vendored
Normal file
BIN
public/js/dssc-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/hashtag.js
vendored
BIN
public/js/hashtag.js
vendored
Binary file not shown.
BIN
public/js/home-ivl9d2teh.js
vendored
BIN
public/js/home-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/home-llsjbikoc.js
vendored
Normal file
BIN
public/js/home-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/installer.js
vendored
BIN
public/js/installer.js
vendored
Binary file not shown.
BIN
public/js/live-player.js
vendored
BIN
public/js/live-player.js
vendored
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
BIN
public/js/notifications-ivl9d2teh.js
vendored
BIN
public/js/notifications-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/notifications-llsjbikoc.js
vendored
Normal file
BIN
public/js/notifications-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/portfolio.js
vendored
Normal file
BIN
public/js/portfolio.js
vendored
Normal file
Binary file not shown.
BIN
public/js/post-ivl9d2teh.js
vendored
BIN
public/js/post-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/post-llsjbikoc.js
vendored
Normal file
BIN
public/js/post-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile-ivl9d2teh.js
vendored
BIN
public/js/profile-ivl9d2teh.js
vendored
Binary file not shown.
BIN
public/js/profile-llsjbikoc.js
vendored
Normal file
BIN
public/js/profile-llsjbikoc.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/rempos.js
vendored
BIN
public/js/rempos.js
vendored
Binary file not shown.
BIN
public/js/rempro.js
vendored
BIN
public/js/rempro.js
vendored
Binary file not shown.
BIN
public/js/search.js
vendored
BIN
public/js/search.js
vendored
Binary file not shown.
BIN
public/js/spa.js
vendored
BIN
public/js/spa.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/stories.js
vendored
BIN
public/js/stories.js
vendored
Binary file not shown.
BIN
public/js/story-compose.js
vendored
BIN
public/js/story-compose.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
Binary file not shown.
122
resources/assets/js/components/PortfolioPost.vue
Normal file
122
resources/assets/js/components/PortfolioPost.vue
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="loading" class="container">
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||||
|
<b-spinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="container mb-5">
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<img :src="post.media_attachments[0].url" class="img-fluid mb-4" style="max-height: 80vh;object-fit: contain;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<p v-if="settings.show_captions && post.content_text">{{ post.content_text }}</p>
|
||||||
|
<div class="d-md-flex justify-content-between align-items-center">
|
||||||
|
<p class="small text-lighter">by <a :href="profileUrl()" class="text-lighter font-weight-bold">@{{profile.username}}</a></p>
|
||||||
|
<p v-if="settings.show_license && post.media_attachments[0].license" class="small text-muted">Licensed under {{ post.media_attachments[0].license.title }}</p>
|
||||||
|
<p v-if="settings.show_location && post.place" class="small text-muted">{{ post.place.name }}, {{ post.place.country }}</p>
|
||||||
|
<p v-if="settings.show_timestamp" class="small text-muted">
|
||||||
|
<a v-if="settings.show_link" :href="post.url" class="text-lighter font-weight-bold" style="z-index: 2">
|
||||||
|
{{ formatDate(post.created_at) }}
|
||||||
|
</a>
|
||||||
|
<span v-else class="user-select-none">
|
||||||
|
{{ formatDate(post.created_at) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex fixed-bottom p-3 justify-content-between align-items-center">
|
||||||
|
<a v-if="user" class="logo-mark logo-mark-sm mb-0 p-1" href="/">
|
||||||
|
<span class="text-gradient-primary">portfolio</span>
|
||||||
|
</a>
|
||||||
|
<span v-else class="logo-mark logo-mark-sm mb-0 p-1">
|
||||||
|
<span class="text-gradient-primary">portfolio</span>
|
||||||
|
</span>
|
||||||
|
<p v-if="user && user.id === profile.id" class="text-center mb-0">
|
||||||
|
<a :href="settingsUrl" class="text-muted"><i class="far fa-cog fa-lg"></i></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
export default {
|
||||||
|
props: [ 'initialData' ],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
isAuthed: undefined,
|
||||||
|
user: undefined,
|
||||||
|
settings: undefined,
|
||||||
|
post: undefined,
|
||||||
|
profile: undefined,
|
||||||
|
settingsUrl: window._portfolio.path + '/settings'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const initialData = JSON.parse(this.initialData);
|
||||||
|
this.post = initialData.post;
|
||||||
|
this.profile = initialData.profile;
|
||||||
|
this.isAuthed = initialData.authed;
|
||||||
|
this.fetchUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async fetchUser() {
|
||||||
|
if(this.isAuthed) {
|
||||||
|
await axios.get('/api/v1/accounts/verify_credentials')
|
||||||
|
.then(res => {
|
||||||
|
this.user = res.data;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await axios.get('/api/portfolio/account/settings.json', {
|
||||||
|
params: {
|
||||||
|
id: this.profile.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.settings = res.data;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
profileUrl() {
|
||||||
|
return `https://${window._portfolio.domain}${window._portfolio.path}/${this.profile.username}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
postUrl(res) {
|
||||||
|
return `/${this.profile.username}/${res.id}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(ts) {
|
||||||
|
const dts = new Date(ts);
|
||||||
|
return dts.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
223
resources/assets/js/components/PortfolioProfile.vue
Normal file
223
resources/assets/js/components/PortfolioProfile.vue
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-100 h-100">
|
||||||
|
<div v-if="loading" class="container">
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||||
|
<b-spinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="container">
|
||||||
|
<div class="row py-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex align-items-center flex-column">
|
||||||
|
<img :src="profile.avatar" width="60" height="60" class="rounded-circle shadow" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
|
|
||||||
|
<div class="py-3 text-center" style="max-width: 60%">
|
||||||
|
<h1 class="font-weight-bold">{{ profile.username }}</h1>
|
||||||
|
<p class="font-weight-light mb-0">{{ profile.note_text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mb-5 pb-5">
|
||||||
|
<div :class="[ settings.profile_layout === 'masonry' ? 'card-columns' : 'row']" id="portContainer">
|
||||||
|
<template v-if="settings.profile_layout ==='grid'">
|
||||||
|
<div v-for="(res, index) in feed" class="col-12 col-md-4 mb-1 p-1">
|
||||||
|
<div class="square">
|
||||||
|
<a :href="postUrl(res)">
|
||||||
|
<img :src="res.media_attachments[0].url" width="100%" height="300" style="overflow: hidden;object-fit: cover;" class="square-content pr-1">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else-if="settings.profile_layout ==='album'" class="col-12 mb-1 p-1">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<p class="text-muted font-weight-bold">{{ albumIndex + 1 }} <span class="font-weight-light">/</span> {{ feed.length }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span v-if="albumIndex === 0">
|
||||||
|
<i class="fa fa-arrow-circle-left fa-3x text-dark" />
|
||||||
|
</span>
|
||||||
|
<a v-else @click.prevent="albumPrev()" href="#">
|
||||||
|
<i class="fa fa-arrow-circle-left fa-3x text-muted"/>
|
||||||
|
</a>
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<a :href="postUrl(feed[albumIndex])" class="mx-4" :key="albumIndex">
|
||||||
|
<img
|
||||||
|
:src="feed[albumIndex].media_attachments[0].url"
|
||||||
|
width="100%"
|
||||||
|
class="user-select-none"
|
||||||
|
style="height: 60vh; overflow: hidden;object-fit: contain;"
|
||||||
|
:draggable="false"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</transition>
|
||||||
|
<span v-if="albumIndex === feed.length - 1">
|
||||||
|
<i class="fa fa-arrow-circle-right fa-3x text-dark" />
|
||||||
|
</span>
|
||||||
|
<a v-else @click.prevent="albumNext()" href="#">
|
||||||
|
<i class="fa fa-arrow-circle-right fa-3x text-muted"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="settings.profile_layout ==='masonry'" class="col-12 p-0 m-0">
|
||||||
|
<div v-for="(res, index) in feed" class="p-1">
|
||||||
|
<a :href="postUrl(res)" data-fancybox="recent" :data-src="res.media_attachments[0].url" :data-width="res.media_attachments[0].width" :data-height="res.media_attachments[0].height">
|
||||||
|
<img
|
||||||
|
:src="res.media_attachments[0].url"
|
||||||
|
width="100%"
|
||||||
|
class="user-select-none"
|
||||||
|
style="overflow: hidden;object-fit: contain;"
|
||||||
|
:draggable="false"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex fixed-bottom p-3 justify-content-between align-items-center">
|
||||||
|
<a v-if="user" class="logo-mark logo-mark-sm mb-0 p-1" href="/">
|
||||||
|
<span class="text-gradient-primary">portfolio</span>
|
||||||
|
</a>
|
||||||
|
<span v-else class="logo-mark logo-mark-sm mb-0 p-1">
|
||||||
|
<span class="text-gradient-primary">portfolio</span>
|
||||||
|
</span>
|
||||||
|
<p v-if="user && user.id == profile.id" class="text-center mb-0">
|
||||||
|
<a :href="settingsUrl" class="text-muted"><i class="far fa-cog fa-lg"></i></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
import '@fancyapps/fancybox/dist/jquery.fancybox.js';
|
||||||
|
import '@fancyapps/fancybox/dist/jquery.fancybox.css';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [ 'initialData' ],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
user: undefined,
|
||||||
|
profile: undefined,
|
||||||
|
settings: undefined,
|
||||||
|
feed: [],
|
||||||
|
albumIndex: 0,
|
||||||
|
settingsUrl: window._portfolio.path + '/settings'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const initialData = JSON.parse(this.initialData);
|
||||||
|
this.profile = initialData.profile;
|
||||||
|
this.fetchUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async fetchUser() {
|
||||||
|
axios.get('/api/v1/accounts/verify_credentials')
|
||||||
|
.then(res => {
|
||||||
|
this.user = res.data;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.get('/api/portfolio/account/settings.json', {
|
||||||
|
params: {
|
||||||
|
id: this.profile.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.settings = res.data;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.fetchFeed();
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchFeed() {
|
||||||
|
axios.get('/api/portfolio/' + this.profile.id + '/feed')
|
||||||
|
.then(res => {
|
||||||
|
this.feed = res.data.filter(p => p.pf_type === "photo");
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.setAlbumSlide();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if(this.settings.profile_layout === 'masonry') {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.initMasonry();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
postUrl(res) {
|
||||||
|
return `${window._portfolio.path}/${this.profile.username}/${res.id}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
albumPrev() {
|
||||||
|
if(this.albumIndex === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.albumIndex === 1) {
|
||||||
|
this.albumIndex--;
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('slide');
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.albumIndex--;
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('slide', this.albumIndex + 1);
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
},
|
||||||
|
|
||||||
|
albumNext() {
|
||||||
|
if(this.albumIndex === this.feed.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.albumIndex++;
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('slide', this.albumIndex + 1);
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
},
|
||||||
|
|
||||||
|
setAlbumSlide() {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
if(url.searchParams.has('slide')) {
|
||||||
|
const slide = Number.parseInt(url.searchParams.get('slide'));
|
||||||
|
if(Number.isNaN(slide)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(slide <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(slide > this.feed.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.albumIndex = url.searchParams.get('slide') - 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initMasonry() {
|
||||||
|
$('[data-fancybox="recent"]').fancybox({
|
||||||
|
gutter: 20,
|
||||||
|
modal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
459
resources/assets/js/components/PortfolioSettings.vue
Normal file
459
resources/assets/js/components/PortfolioSettings.vue
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
<template>
|
||||||
|
<div class="portfolio-settings px-3">
|
||||||
|
<div v-if="loading" class="d-flex justify-content-center align-items-center py-5">
|
||||||
|
<b-spinner variant="primary" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="row justify-content-center mb-5 pb-5">
|
||||||
|
<div class="col-12 col-md-8 bg-dark py-2 rounded">
|
||||||
|
<ul class="nav nav-pills nav-fill">
|
||||||
|
<li v-for="(tab, index) in tabs" class="nav-item" :class="{ disabled: index !== 0 && !settings.active}">
|
||||||
|
<span v-if="index !== 0 && !settings.active" class="nav-link">{{ tab }}</span>
|
||||||
|
<a v-else class="nav-link" :class="{ active: tab === tabIndex }" href="#" @click.prevent="toggleTab(tab)">{{ tab }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<div v-if="tabIndex === 'Configure'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
|
||||||
|
<div v-if="!user.statuses_count" class="alert alert-danger">
|
||||||
|
<p class="mb-0 small font-weight-bold">You don't have any public posts, once you share public posts you can enable your portfolio.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="lead mb-0">Portfolio Enabled</p>
|
||||||
|
<p class="small mb-0 text-muted">You must enable your portfolio before you or anyone can view it.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-switch mt-n1">
|
||||||
|
<b-form-checkbox v-model="settings.active" name="check-button" size="lg" switch :disabled="!user.statuses_count" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label" style="max-width: 50%;">
|
||||||
|
<p class="mb-0">Portfolio Source</p>
|
||||||
|
<p class="small mb-0 text-muted">Choose how you want to populate your portfolio, select Most Recent posts to automatically update your portfolio with recent posts or Curated Posts to select specific posts for your portfolio.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<b-form-select v-model="settings.profile_source" :options="profileSourceOptions" :disabled="!user.statuses_count" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tabIndex === 'Curate'" class="col-12 col-md-8 mt-3 py-2 px-0" key="1">
|
||||||
|
<div v-if="!recentPostsLoaded" class="d-flex align-items-center justify-content-center py-5 my-5">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted">Loading recent posts...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="mt-n2 mb-4">
|
||||||
|
<p class="text-muted small">Select up to 24 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/24</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-link font-weight-bold mr-3 text-decoration-none"
|
||||||
|
:disabled="!selectedRecentPosts.length"
|
||||||
|
@click="clearSelected">
|
||||||
|
Clear selected
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary py-0 font-weight-bold"
|
||||||
|
style="width: 150px;"
|
||||||
|
:disabled="!canSaveCurated"
|
||||||
|
@click="saveCurated()">
|
||||||
|
<template v-if="!isSavingCurated">Save</template>
|
||||||
|
<b-spinner v-else small />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span @click="recentPostsPrev">
|
||||||
|
<i :class="prevClass" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="row flex-grow-1 mx-2">
|
||||||
|
<div v-for="(post, index) in recentPosts.slice(rpStart, rpStart + 9)" class="col-12 col-md-4 mb-1 p-1">
|
||||||
|
<div class="square user-select-none" @click.prevent="toggleRecentPost(post.id)">
|
||||||
|
<transition name="fade">
|
||||||
|
<img
|
||||||
|
:key="post.id"
|
||||||
|
:src="post.media_attachments[0].url"
|
||||||
|
width="100%"
|
||||||
|
height="300"
|
||||||
|
style="overflow: hidden;object-fit: cover;"
|
||||||
|
:draggable="false"
|
||||||
|
class="square-content pr-1">
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div v-if="selectedRecentPosts.indexOf(post.id) !== -1" style="position: absolute;right: -5px;bottom:-5px;">
|
||||||
|
<div class="selected-badge">{{ selectedRecentPosts.indexOf(post.id) + 1 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span @click="recentPostsNext()">
|
||||||
|
<i :class="nextClass" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tabIndex === 'Customize'" class="col-12 col-md-8 mt-3 py-2" key="2">
|
||||||
|
<div v-for="setting in customizeSettings" class="card bg-dark mb-5">
|
||||||
|
<div class="card-header">{{ setting.title }}</div>
|
||||||
|
<div class="list-group bg-dark">
|
||||||
|
<div v-for="item in setting.items" class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">{{ item.label }}</p>
|
||||||
|
<p v-if="item.description" class="small text-muted mb-0">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-switch mt-n1">
|
||||||
|
<b-form-checkbox
|
||||||
|
v-model="settings[item.model]"
|
||||||
|
name="check-button"
|
||||||
|
size="lg"
|
||||||
|
switch
|
||||||
|
:disabled="item.requiredWithTrue && !settings[item.requiredWithTrue]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card bg-dark mb-5">
|
||||||
|
<div class="card-header">Portfolio</div>
|
||||||
|
<div class="list-group bg-dark">
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Layout</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b-form-select v-model="settings.profile_layout" :options="profileLayoutOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tabIndex === 'Share'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
|
||||||
|
<div class="py-2">
|
||||||
|
<p class="text-muted">Portfolio URL</p>
|
||||||
|
<p class="lead mb-0"><a :href="settings.url">{{ settings.url }}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
tabIndex: "Configure",
|
||||||
|
tabs: [
|
||||||
|
"Configure",
|
||||||
|
"Customize",
|
||||||
|
"View Portfolio"
|
||||||
|
],
|
||||||
|
user: undefined,
|
||||||
|
settings: undefined,
|
||||||
|
recentPostsLoaded: false,
|
||||||
|
rpStart: 0,
|
||||||
|
recentPosts: [],
|
||||||
|
recentPostsPage: undefined,
|
||||||
|
selectedRecentPosts: [],
|
||||||
|
isSavingCurated: false,
|
||||||
|
canSaveCurated: false,
|
||||||
|
customizeSettings: [],
|
||||||
|
profileSourceOptions: [
|
||||||
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
|
{ value: 'recent', text: 'Most recent posts' },
|
||||||
|
],
|
||||||
|
profileLayoutOptions: [
|
||||||
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
|
{ value: 'grid', text: 'Grid' },
|
||||||
|
{ value: 'masonry', text: 'Masonry' },
|
||||||
|
{ value: 'album', text: 'Album' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
prevClass() {
|
||||||
|
return this.rpStart === 0 ?
|
||||||
|
"fa fa-arrow-circle-left fa-3x text-dark" :
|
||||||
|
"fa fa-arrow-circle-left fa-3x text-muted cursor-pointer";
|
||||||
|
},
|
||||||
|
|
||||||
|
nextClass() {
|
||||||
|
return this.rpStart > (this.recentPosts.length - 9) ?
|
||||||
|
"fa fa-arrow-circle-right fa-3x text-dark" :
|
||||||
|
"fa fa-arrow-circle-right fa-3x text-muted cursor-pointer";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
settings: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler: function(o, n) {
|
||||||
|
if(this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!n.show_timestamp) {
|
||||||
|
this.settings.show_link = false;
|
||||||
|
}
|
||||||
|
this.updateSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchUser() {
|
||||||
|
axios.get('/api/v1/accounts/verify_credentials')
|
||||||
|
.then(res => {
|
||||||
|
this.user = res.data;
|
||||||
|
|
||||||
|
if(res.data.statuses_count > 0) {
|
||||||
|
this.profileSourceOptions = [
|
||||||
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
|
{ value: 'recent', text: 'Most recent posts' },
|
||||||
|
{ value: 'custom', text: 'Curated posts' },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.settings.active = false;
|
||||||
|
this.settings.profile_source = 'recent';
|
||||||
|
this.tabIndex = 'Configure';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
axios.post(this.apiPath('/api/portfolio/self/settings.json'))
|
||||||
|
.then(res => {
|
||||||
|
this.settings = res.data;
|
||||||
|
this.updateTabs();
|
||||||
|
if(res.data.metadata && res.data.metadata.posts) {
|
||||||
|
this.selectedRecentPosts = res.data.metadata.posts;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.initCustomizeSettings();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
if(url.searchParams.has('tab')) {
|
||||||
|
let tab = url.searchParams.get('tab');
|
||||||
|
let tabs = this.settings.profile_source === 'custom' ?
|
||||||
|
['curate', 'customize', 'share'] :
|
||||||
|
['customize', 'share'];
|
||||||
|
if(tabs.indexOf(tab) !== -1) {
|
||||||
|
this.toggleTab(tab.slice(0, 1).toUpperCase() + tab.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
apiPath(path) {
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTab(idx) {
|
||||||
|
if(idx === 'Curate' && !this.recentPostsLoaded) {
|
||||||
|
this.loadRecentPosts();
|
||||||
|
}
|
||||||
|
this.tabIndex = idx;
|
||||||
|
this.rpStart = 0;
|
||||||
|
if(idx == 'Configure') {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('tab');
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
} else if (idx == 'View Portfolio') {
|
||||||
|
this.tabIndex = 'Configure';
|
||||||
|
window.location.href = `https://${window._portfolio.domain}${window._portfolio.path}/${this.user.username}`;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('tab', idx.toLowerCase());
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTabs() {
|
||||||
|
if(this.settings.profile_source === 'custom') {
|
||||||
|
this.tabs = [
|
||||||
|
"Configure",
|
||||||
|
"Curate",
|
||||||
|
"Customize",
|
||||||
|
"View Portfolio"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this.tabs = [
|
||||||
|
"Configure",
|
||||||
|
"Customize",
|
||||||
|
"View Portfolio"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSettings() {
|
||||||
|
axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
|
||||||
|
.then(res => {
|
||||||
|
this.updateTabs();
|
||||||
|
this.$bvToast.toast(`Your settings have been successfully updated!`, {
|
||||||
|
variant: 'dark',
|
||||||
|
title: 'Settings Updated',
|
||||||
|
autoHideDelay: 2000,
|
||||||
|
appendToast: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
loadRecentPosts() {
|
||||||
|
axios.get('/api/v1/accounts/' + this.user.id + '/statuses?only_media=1&media_types=photo&limit=100')
|
||||||
|
.then(res => {
|
||||||
|
if(res.data.length) {
|
||||||
|
this.recentPosts = res.data.filter(p => p.visibility === "public");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.recentPostsLoaded = true;
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleRecentPost(id) {
|
||||||
|
if(this.selectedRecentPosts.indexOf(id) == -1) {
|
||||||
|
if(this.selectedRecentPosts.length === 24) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedRecentPosts.push(id);
|
||||||
|
} else {
|
||||||
|
this.selectedRecentPosts = this.selectedRecentPosts.filter(i => i !== id);
|
||||||
|
}
|
||||||
|
this.canSaveCurated = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
recentPostsPrev() {
|
||||||
|
if(this.rpStart === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rpStart = this.rpStart - 9;
|
||||||
|
},
|
||||||
|
|
||||||
|
recentPostsNext() {
|
||||||
|
if(this.rpStart > (this.recentPosts.length - 9)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rpStart = this.rpStart + 9;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSelected() {
|
||||||
|
this.selectedRecentPosts = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCurated() {
|
||||||
|
this.isSavingCurated = true;
|
||||||
|
event.currentTarget?.blur();
|
||||||
|
|
||||||
|
axios.post('/api/portfolio/self/curated.json', {
|
||||||
|
ids: this.selectedRecentPosts
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.isSavingCurated = false;
|
||||||
|
this.$bvToast.toast(`Your curated posts have been updated!`, {
|
||||||
|
variant: 'dark',
|
||||||
|
title: 'Portfolio Updated',
|
||||||
|
autoHideDelay: 2000,
|
||||||
|
appendToast: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.isSavingCurated = false;
|
||||||
|
this.$bvToast.toast(`An error occured while attempting to update your portfolio, please try again later and contact an admin if this problem persists.`, {
|
||||||
|
variant: 'dark',
|
||||||
|
title: 'Error',
|
||||||
|
autoHideDelay: 2000,
|
||||||
|
appendToast: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
initCustomizeSettings() {
|
||||||
|
this.customizeSettings = [
|
||||||
|
{
|
||||||
|
title: "Post Settings",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Show Captions",
|
||||||
|
model: "show_captions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Show License",
|
||||||
|
model: "show_license"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Show Location",
|
||||||
|
model: "show_location"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Show Timestamp",
|
||||||
|
model: "show_timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Link to Post",
|
||||||
|
description: "Add link to timestamp to view the original post url, requires show timestamp to be enabled",
|
||||||
|
model: "show_link",
|
||||||
|
requiredWithTrue: "show_timestamp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "Profile Settings",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Show Avatar",
|
||||||
|
model: "show_avatar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Show Bio",
|
||||||
|
model: "show_bio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
19
resources/assets/js/portfolio.js
vendored
Normal file
19
resources/assets/js/portfolio.js
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
window.Vue = Vue;
|
||||||
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
Vue.use(BootstrapVue);
|
||||||
|
|
||||||
|
Vue.component(
|
||||||
|
'portfolio-post',
|
||||||
|
require('./components/PortfolioPost.vue').default
|
||||||
|
);
|
||||||
|
|
||||||
|
Vue.component(
|
||||||
|
'portfolio-profile',
|
||||||
|
require('./components/PortfolioProfile.vue').default
|
||||||
|
);
|
||||||
|
|
||||||
|
Vue.component(
|
||||||
|
'portfolio-settings',
|
||||||
|
require('./components/PortfolioSettings.vue').default
|
||||||
|
);
|
54
resources/assets/sass/lib/inter.scss
vendored
Normal file
54
resources/assets/sass/lib/inter.scss
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
173
resources/assets/sass/portfolio.scss
vendored
Normal file
173
resources/assets/sass/portfolio.scss
vendored
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
@import "lib/inter";
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #000000;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
color: #d4d4d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #3B82F6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead,
|
||||||
|
.font-weight-light {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3B82F6;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-primary {
|
||||||
|
background: linear-gradient(to right, #6366f1, #8B5CF6, #D946EF);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: rgba(0,0,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-mark {
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif!important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
letter-spacing: -1.5px;
|
||||||
|
border: 6px solid #212529;
|
||||||
|
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
background: #212529;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-sm {
|
||||||
|
font-size: 16px !important;
|
||||||
|
border-width: 3px;
|
||||||
|
border-radius: 10px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
background: #212529;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-4.font-weight-bold {
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
letter-spacing: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #d1d5db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-columns {
|
||||||
|
-moz-column-count: 3;
|
||||||
|
column-count: 3;
|
||||||
|
-moz-column-gap: 0px;
|
||||||
|
column-gap: 0px;
|
||||||
|
orphans: 1;
|
||||||
|
widows: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-settings {
|
||||||
|
.nav-pills {
|
||||||
|
.nav-item {
|
||||||
|
&.disabled {
|
||||||
|
span {
|
||||||
|
pointer-events: none;
|
||||||
|
color: #3f3f46;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background-image: linear-gradient(to right, #4f46e5 0%, #2F80ED 51%, #4f46e5 100%);
|
||||||
|
background-size: 200% auto;
|
||||||
|
font-weight: 100;
|
||||||
|
transition: 0.5s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-position: right center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
&-header {
|
||||||
|
background-color: #000;
|
||||||
|
border: 1px solid var(--dark);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: #fff;
|
||||||
|
background: #000 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat;
|
||||||
|
border-color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-badge {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 26px;
|
||||||
|
background-color: #0284c7;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all .3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all .3s cubic-bezier(1.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter, .slide-fade-leave-to {
|
||||||
|
transform: translateX(10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<nav class="navbar navbar-expand navbar-light navbar-laravel shadow-none border-bottom sticky-top py-1">
|
<nav class="navbar navbar-expand navbar-light navbar-laravel shadow-none border-bottom sticky-top py-1">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand d-flex align-items-center" href="/" title="Logo">
|
<a class="navbar-brand d-flex align-items-center" href="{{ config('app.url') }}" title="Logo">
|
||||||
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager" alt="Pixelfed logo">
|
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager" alt="Pixelfed logo">
|
||||||
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config_cache('app.name') }}</span>
|
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config_cache('app.name') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
21
resources/views/portfolio/404.blade.php
Normal file
21
resources/views/portfolio/404.blade.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
@extends('portfolio.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mt-5 pt-5">
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<p class="mb-5">
|
||||||
|
<span class="logo-mark px-3"><span class="text-gradient-primary">portfolio</span></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1>404 - Not Found</h1>
|
||||||
|
|
||||||
|
<p class="lead pt-3 mb-4">This portfolio or post is either not active or has been removed.</p>
|
||||||
|
|
||||||
|
<p class="mt-3">
|
||||||
|
<a href="{{ config('app.url') }}" class="text-muted" style="text-decoration: underline;">Go back home</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
36
resources/views/portfolio/index.blade.php
Normal file
36
resources/views/portfolio/index.blade.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
@extends('portfolio.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center mt-5 pt-5">
|
||||||
|
<div class="col-12 col-md-6 text-center">
|
||||||
|
<p class="mb-3">
|
||||||
|
<span class="logo-mark px-3"><span class="text-gradient-primary">portfolio</span></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="spinner-border mt-5" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript">
|
||||||
|
@auth
|
||||||
|
axios.get('/api/v1/accounts/verify_credentials')
|
||||||
|
.then(res => {
|
||||||
|
if(res.data.locked == false) {
|
||||||
|
window.location.href = 'https://{{ config('portfolio.domain') }}{{ config('portfolio.path') }}/' + res.data.username
|
||||||
|
} else {
|
||||||
|
window.location.href = "{{ config('app.url') }}";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@else
|
||||||
|
window.location.href = "{{ config('app.url') }}";
|
||||||
|
@endauth
|
||||||
|
|
||||||
|
</script>
|
||||||
|
@endpush
|
40
resources/views/portfolio/layout.blade.php
Normal file
40
resources/views/portfolio/layout.blade.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ app()->getLocale() }}">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<title>{!! $title ?? config_cache('app.name') !!}</title>
|
||||||
|
|
||||||
|
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
|
||||||
|
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:url" content="{{request()->url()}}">
|
||||||
|
@stack('meta')
|
||||||
|
|
||||||
|
<meta name="medium" content="image">
|
||||||
|
<meta name="theme-color" content="#10c5f8">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
|
||||||
|
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
|
||||||
|
<link rel="canonical" href="{{request()->url()}}">
|
||||||
|
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
|
||||||
|
<link href="{{ mix('css/portfolio.css') }}" rel="stylesheet" data-stylesheet="light">
|
||||||
|
<script type="text/javascript">window._portfolio = { domain: "{{config('portfolio.domain')}}", path: "{{config('portfolio.path')}}"}</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="w-100 h-100">
|
||||||
|
<main id="content" class="w-100 h-100">
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
23
resources/views/portfolio/settings.blade.php
Normal file
23
resources/views/portfolio/settings.blade.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@extends('portfolio.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mt-5 pt-5 px-0 align-items-center">
|
||||||
|
<div class="col-12 mb-5 col-md-8">
|
||||||
|
<span class="logo-mark px-3"><span class="text-gradient-primary">portfolio</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-5 col-md-4 text-md-right">
|
||||||
|
<h1 class="font-weight-bold">Settings</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<portfolio-settings />
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript" src="{{ mix('js/portfolio.js') }}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
App.boot();
|
||||||
|
</script>
|
||||||
|
@endpush
|
12
resources/views/portfolio/show.blade.php
Normal file
12
resources/views/portfolio/show.blade.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@extends('portfolio.layout', ['title' => "@{$user['username']}'s Portfolio"])
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<portfolio-profile initial-data="{{json_encode(['profile' => $user])}}" />
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript" src="{{ mix('js/portfolio.js') }}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
App.boot();
|
||||||
|
</script>
|
||||||
|
@endpush
|
17
resources/views/portfolio/show_post.blade.php
Normal file
17
resources/views/portfolio/show_post.blade.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@extends('portfolio.layout', ['title' => "@{$user['username']}'s Portfolio Photo"])
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<portfolio-post initial-data="{{json_encode(['profile' => $user, 'post' => $post, 'authed' => $authed ? true : false])}}" />
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript" src="{{ mix('js/portfolio.js') }}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
App.boot();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('meta')<meta property="og:description" content="{{ $post['content_text'] }}">
|
||||||
|
<meta property="og:image" content="{{ $post['media_attachments'][0]['url']}}">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
@endpush
|
|
@ -100,6 +100,28 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::domain(config('portfolio.domain'))->group(function () {
|
||||||
|
Route::redirect('redirect/home', config('app.url'));
|
||||||
|
Route::get('/', 'PortfolioController@index');
|
||||||
|
Route::post('api/portfolio/self/curated.json', 'PortfolioController@storeCurated');
|
||||||
|
Route::post('api/portfolio/self/settings.json', 'PortfolioController@getSettings');
|
||||||
|
Route::get('api/portfolio/account/settings.json', 'PortfolioController@getAccountSettings');
|
||||||
|
Route::post('api/portfolio/self/update-settings.json', 'PortfolioController@storeSettings');
|
||||||
|
Route::get('api/portfolio/{username}/feed', 'PortfolioController@getFeed');
|
||||||
|
|
||||||
|
Route::prefix(config('portfolio.path'))->group(function() {
|
||||||
|
Route::get('/', 'PortfolioController@index');
|
||||||
|
Route::get('settings', 'PortfolioController@settings')->name('portfolio.settings');
|
||||||
|
Route::post('settings', 'PortfolioController@store');
|
||||||
|
Route::get('{username}/{id}', 'PortfolioController@showPost');
|
||||||
|
Route::get('{username}', 'PortfolioController@show');
|
||||||
|
|
||||||
|
Route::fallback(function () {
|
||||||
|
return view('errors.404');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
|
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
|
||||||
Route::get('/', 'SiteController@home')->name('timeline.personal');
|
Route::get('/', 'SiteController@home')->name('timeline.personal');
|
||||||
|
|
||||||
|
@ -268,6 +290,14 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
||||||
Route::post('v1/publish', 'StoryController@publishStory');
|
Route::post('v1/publish', 'StoryController@publishStory');
|
||||||
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete');
|
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::group(['prefix' => 'portfolio'], function () {
|
||||||
|
Route::post('self/curated.json', 'PortfolioController@storeCurated');
|
||||||
|
Route::post('self/settings.json', 'PortfolioController@getSettings');
|
||||||
|
Route::get('account/settings.json', 'PortfolioController@getAccountSettings');
|
||||||
|
Route::post('self/update-settings.json', 'PortfolioController@storeSettings');
|
||||||
|
Route::get('{username}/feed', 'PortfolioController@getFeed');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
|
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
|
||||||
|
@ -352,6 +382,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
||||||
Route::post('warning', 'AccountInterstitialController@read');
|
Route::post('warning', 'AccountInterstitialController@read');
|
||||||
Route::get('my2020', 'SeasonalController@yearInReview');
|
Route::get('my2020', 'SeasonalController@yearInReview');
|
||||||
|
|
||||||
|
Route::get('web/my-portfolio', 'PortfolioController@myRedirect');
|
||||||
Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect');
|
Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect');
|
||||||
Route::get('web/username/{id}', 'SpaController@usernameRedirect');
|
Route::get('web/username/{id}', 'SpaController@usernameRedirect');
|
||||||
Route::get('web/post/{id}', 'SpaController@webPost');
|
Route::get('web/post/{id}', 'SpaController@webPost');
|
||||||
|
|
Loading…
Reference in a new issue