1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/pixelfed_ynh.git synced 2024-09-03 20:06:04 +02:00

Merge pull request #3705 from pixelfed/staging

Add Portfolio feature
This commit is contained in:
daniel 2022-10-17 02:30:55 -06:00 committed by GitHub
commit 14a1a0283c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1742 additions and 72 deletions

View file

@ -2,6 +2,9 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.4...dev)
### New Features
- Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705))
### Updates
- 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))

View 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
View 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
View 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'),
];

View file

@ -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');
}
}

1
public/css/portfolio.css vendored Normal file
View file

@ -0,0 +1 @@
@font-face{font-display:swap;font-family:Inter;font-style:normal;font-weight:100;src:url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-display:swap;font-family:Inter;font-style:normal;font-weight:100;src:url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format("woff2");unicode-range:U+00??,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}@font-face{font-display:swap;font-family:Inter;font-style:normal;font-weight:400;src:url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-display:swap;font-family:Inter;font-style:normal;font-weight:400;src:url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format("woff2");unicode-range:U+00??,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}@font-face{font-display:swap;font-family:Inter;font-style:normal;font-weight:700;src:url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-display:swap;font-family:Inter;font-style:normal;font-weight:700;src:url(/fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format("woff2");unicode-range:U+00??,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}body{background:#000;color:#d4d4d8;font-family:Inter,sans-serif;font-weight:400!important}.text-primary{color:#3b82f6!important}.font-weight-light,.lead{font-weight:400!important}a{color:#3b82f6;text-decoration:none}.text-gradient-primary{-webkit-text-fill-color:transparent;background:linear-gradient(90deg,#6366f1,#8b5cf6,#d946ef);-webkit-background-clip:text}.logo-mark{background:#212529;border:6px solid #212529;border-radius:1rem;color:#fff!important;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif!important;font-size:2.5rem;font-weight:700!important;letter-spacing:-1.5px;line-height:1.2;text-decoration:none!important;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media(min-width:768px){.logo-mark{font-size:4.5rem}}.logo-mark-sm{background:#212529;border-radius:10px;border-width:3px;font-size:16px!important;letter-spacing:-1px}.display-4.font-weight-bold{letter-spacing:-.3px;text-transform:uppercase}@media(min-width:768px){.display-4.font-weight-bold{letter-spacing:-3px}}.display-4.font-weight-bold a{color:#d1d5db;text-decoration:underline}.display-4{font-size:1.5rem}@media(min-width:768px){.display-4{font-size:3.5rem}}.btn-primary{background-color:#3b82f6}.card-columns{-moz-column-count:3;column-count:3;-moz-column-gap:0;column-gap:0;orphans:1;widows:1}.portfolio-settings .nav-pills .nav-item.disabled span{color:#3f3f46;pointer-events:none}.portfolio-settings .nav-pills .nav-link{color:#9ca3af;font-size:15px;font-weight:400}.portfolio-settings .nav-pills .nav-link.active{background-image:linear-gradient(90deg,#4f46e5 0,#2f80ed 51%,#4f46e5);background-size:200% auto;color:#fff;font-weight:100;transition:.5s}.portfolio-settings .nav-pills .nav-link.active:hover{background-position:100%}.portfolio-settings .card-header{background-color:#000;border:1px solid var(--dark);color:var(--muted);font-size:14px;font-weight:400;text-transform:uppercase}.portfolio-settings .card .list-group-item{background:transparent}.portfolio-settings .custom-select{background:#000 url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%23343a40' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat;border-color:var(--dark);border-radius:10px;color:#fff;font-weight:700;padding-left:20px}.portfolio-settings .selected-badge{align-items:center;background-color:#0284c7;border:2px solid #fff;border-radius:26px;color:#fff;display:flex;font-size:14px;font-weight:700;height:26px;justify-content:center;width:26px}.slide-fade-enter-active{transition:all .3s ease}.slide-fade-leave-active{transition:all .3s cubic-bezier(1,1)}.slide-fade-enter,.slide-fade-leave-to{opacity:0;transform:translateX(10px)}

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
"use strict";(self.webpackChunkpixelfed=self.webpackChunkpixelfed||[]).push([[8],{9324:(e,t,o)=>{o.r(t);var a=o(70538),l=o(25518),n=o(30306),r=o.n(n),s=o(7398),d=o.n(s),c=o(92987),i=o(37409),u=o.n(i),f=o(74870),h=o.n(f),m=(o(82711),o(46737),o(19755));function p(e){return p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},p(e)}window.Vue=a.default,a.default.use(h()),a.default.use(u()),a.default.use(l.default),a.default.use(r()),a.default.use(d()),a.default.use(c.default,{name:"Timeago",locale:"en"}),pixelfed.readmore=function(){m(".read-more").each((function(e,t){var o=m(this),a=o.attr("data-readmore");"undefined"!==p(a)&&!1!==a||o.readmore({collapsedHeight:45,heightMargin:48,moreLink:'<a href="#" class="d-block small font-weight-bold text-dark text-center">Show more</a>',lessLink:'<a href="#" class="d-block small font-weight-bold text-dark text-center">Show less</a>'})}))};try{document.createEvent("TouchEvent"),m("body").addClass("touch")}catch(e){}window.filesize=o(42317),m('[data-toggle="tooltip"]').tooltip();console.log("%cStop!","color:red; font-size:60px; font-weight: bold; -webkit-text-stroke: 1px black;"),console.log('%cThis is a browser feature intended for developers. If someone told you to copy and paste something here to enable a Pixelfed feature or "hack" someone\'s account, it is a scam and will give them access to your Pixelfed account.',"font-size: 18px;")}},e=>{e.O(0,[898],(()=>{return t=9324,e(e.s=t);var t}));e.O()}]);
"use strict";(self.webpackChunkpixelfed=self.webpackChunkpixelfed||[]).push([[8],{9324:(e,t,o)=>{o.r(t);var a=o(70538),l=o(25518),n=o(30306),r=o.n(n),s=o(7398),d=o.n(s),c=o(92987),i=o(37409),u=o.n(i),f=o(74870),h=o.n(f),m=(o(93142),o(46737),o(19755));function p(e){return p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},p(e)}window.Vue=a.default,a.default.use(h()),a.default.use(u()),a.default.use(l.default),a.default.use(r()),a.default.use(d()),a.default.use(c.default,{name:"Timeago",locale:"en"}),pixelfed.readmore=function(){m(".read-more").each((function(e,t){var o=m(this),a=o.attr("data-readmore");"undefined"!==p(a)&&!1!==a||o.readmore({collapsedHeight:45,heightMargin:48,moreLink:'<a href="#" class="d-block small font-weight-bold text-dark text-center">Show more</a>',lessLink:'<a href="#" class="d-block small font-weight-bold text-dark text-center">Show less</a>'})}))};try{document.createEvent("TouchEvent"),m("body").addClass("touch")}catch(e){}window.filesize=o(42317),m('[data-toggle="tooltip"]').tooltip();console.log("%cStop!","color:red; font-size:60px; font-weight: bold; -webkit-text-stroke: 1px black;"),console.log('%cThis is a browser feature intended for developers. If someone told you to copy and paste something here to enable a Pixelfed feature or "hack" someone\'s account, it is a scam and will give them access to your Pixelfed account.',"font-size: 18px;")}},e=>{e.O(0,[898],(()=>{return t=9324,e(e.s=t);var t}));e.O()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/compose-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/daci-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dffc-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

2
public/js/direct.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/discover-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dms-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dmsg-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dmyh-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dmym-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dsfc-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/dssc-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/home-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
(()=>{"use strict";var e,t,r,s={},i={};function o(e){var t=i[e];if(void 0!==t)return t.exports;var r=i[e]={id:e,loaded:!1,exports:{}};return s[e].call(r.exports,r,r.exports,o),r.loaded=!0,r.exports}o.m=s,e=[],o.O=(t,r,s,i)=>{if(!r){var n=1/0;for(c=0;c<e.length;c++){for(var[r,s,i]=e[c],d=!0,l=0;l<r.length;l++)(!1&i||n>=i)&&Object.keys(o.O).every((e=>o.O[e](r[l])))?r.splice(l--,1):(d=!1,i<n&&(n=i));if(d){e.splice(c--,1);var a=s();void 0!==a&&(t=a)}}return t}i=i||0;for(var c=e.length;c>0&&e[c-1][2]>i;c--)e[c]=e[c-1];e[c]=[r,s,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce(((t,r)=>(o.f[r](e,t),t)),[])),o.u=e=>113===e?"js/home-ivl9d2teh.js":844===e?"js/compose-ivl9d2teh.js":635===e?"js/post-ivl9d2teh.js":761===e?"js/profile-ivl9d2teh.js":51===e?"js/dmym-ivl9d2teh.js":40===e?"js/dmyh-ivl9d2teh.js":200===e?"js/daci-ivl9d2teh.js":576===e?"js/dffc-ivl9d2teh.js":788===e?"js/dsfc-ivl9d2teh.js":382===e?"js/dssc-ivl9d2teh.js":854===e?"js/discover-ivl9d2teh.js":13===e?"js/notifications-ivl9d2teh.js":641===e?"js/dms-ivl9d2teh.js":732===e?"js/dmsg-ivl9d2teh.js":void 0,o.miniCssF=e=>({138:"css/spa",170:"css/app",242:"css/appdark",703:"css/admin",994:"css/landing"}[e]+".css"),o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="pixelfed:",o.l=(e,s,i,n)=>{if(t[e])t[e].push(s);else{var d,l;if(void 0!==i)for(var a=document.getElementsByTagName("script"),c=0;c<a.length;c++){var u=a[c];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==r+i){d=u;break}}d||(l=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,o.nc&&d.setAttribute("nonce",o.nc),d.setAttribute("data-webpack",r+i),d.src=e),t[e]=[s];var f=(r,s)=>{d.onerror=d.onload=null,clearTimeout(p);var i=t[e];if(delete t[e],d.parentNode&&d.parentNode.removeChild(d),i&&i.forEach((e=>e(s))),r)return r(s)},p=setTimeout(f.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=f.bind(null,d.onerror),d.onload=f.bind(null,d.onload),l&&document.head.appendChild(d)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.p="/",(()=>{var e={929:0,242:0,170:0,138:0,703:0,994:0};o.f.j=(t,r)=>{var s=o.o(e,t)?e[t]:void 0;if(0!==s)if(s)r.push(s[2]);else if(/^(138|170|242|703|929|994)$/.test(t))e[t]=0;else{var i=new Promise(((r,i)=>s=e[t]=[r,i]));r.push(s[2]=i);var n=o.p+o.u(t),d=new Error;o.l(n,(r=>{if(o.o(e,t)&&(0!==(s=e[t])&&(e[t]=void 0),s)){var i=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;d.message="Loading chunk "+t+" failed.\n("+i+": "+n+")",d.name="ChunkLoadError",d.type=i,d.request=n,s[1](d)}}),"chunk-"+t,t)}},o.O.j=t=>0===e[t];var t=(t,r)=>{var s,i,[n,d,l]=r,a=0;if(n.some((t=>0!==e[t]))){for(s in d)o.o(d,s)&&(o.m[s]=d[s]);if(l)var c=l(o)}for(t&&t(r);a<n.length;a++)i=n[a],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(c)},r=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),o.nc=void 0})();
(()=>{"use strict";var e,r,s,o={},t={};function i(e){var r=t[e];if(void 0!==r)return r.exports;var s=t[e]={id:e,loaded:!1,exports:{}};return o[e].call(s.exports,s,s.exports,i),s.loaded=!0,s.exports}i.m=o,e=[],i.O=(r,s,o,t)=>{if(!s){var l=1/0;for(d=0;d<e.length;d++){for(var[s,o,t]=e[d],n=!0,a=0;a<s.length;a++)(!1&t||l>=t)&&Object.keys(i.O).every((e=>i.O[e](s[a])))?s.splice(a--,1):(n=!1,t<l&&(l=t));if(n){e.splice(d--,1);var c=o();void 0!==c&&(r=c)}}return r}t=t||0;for(var d=e.length;d>0&&e[d-1][2]>t;d--)e[d]=e[d-1];e[d]=[s,o,t]},i.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return i.d(r,{a:r}),r},i.d=(e,r)=>{for(var s in r)i.o(r,s)&&!i.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:r[s]})},i.f={},i.e=e=>Promise.all(Object.keys(i.f).reduce(((r,s)=>(i.f[s](e,r),r)),[])),i.u=e=>17===e?"js/home-llsjbikoc.js":434===e?"js/compose-llsjbikoc.js":121===e?"js/post-llsjbikoc.js":825===e?"js/profile-llsjbikoc.js":472===e?"js/dmym-llsjbikoc.js":464===e?"js/dmyh-llsjbikoc.js":206===e?"js/daci-llsjbikoc.js":831===e?"js/dffc-llsjbikoc.js":661===e?"js/dsfc-llsjbikoc.js":310===e?"js/dssc-llsjbikoc.js":731===e?"js/discover-llsjbikoc.js":921===e?"js/notifications-llsjbikoc.js":379===e?"js/dms-llsjbikoc.js":875===e?"js/dmsg-llsjbikoc.js":void 0,i.miniCssF=e=>({138:"css/spa",170:"css/app",242:"css/appdark",703:"css/admin",737:"css/portfolio",994:"css/landing"}[e]+".css"),i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),i.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},s="pixelfed:",i.l=(e,o,t,l)=>{if(r[e])r[e].push(o);else{var n,a;if(void 0!==t)for(var c=document.getElementsByTagName("script"),d=0;d<c.length;d++){var u=c[d];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==s+t){n=u;break}}n||(a=!0,(n=document.createElement("script")).charset="utf-8",n.timeout=120,i.nc&&n.setAttribute("nonce",i.nc),n.setAttribute("data-webpack",s+t),n.src=e),r[e]=[o];var j=(s,o)=>{n.onerror=n.onload=null,clearTimeout(f);var t=r[e];if(delete r[e],n.parentNode&&n.parentNode.removeChild(n),t&&t.forEach((e=>e(o))),s)return s(o)},f=setTimeout(j.bind(null,void 0,{type:"timeout",target:n}),12e4);n.onerror=j.bind(null,n.onerror),n.onload=j.bind(null,n.onload),a&&document.head.appendChild(n)}},i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),i.p="/",(()=>{var e={929:0,242:0,170:0,737:0,703:0,994:0,138:0};i.f.j=(r,s)=>{var o=i.o(e,r)?e[r]:void 0;if(0!==o)if(o)s.push(o[2]);else if(/^(138|170|242|703|737|929|994)$/.test(r))e[r]=0;else{var t=new Promise(((s,t)=>o=e[r]=[s,t]));s.push(o[2]=t);var l=i.p+i.u(r),n=new Error;i.l(l,(s=>{if(i.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var t=s&&("load"===s.type?"missing":s.type),l=s&&s.target&&s.target.src;n.message="Loading chunk "+r+" failed.\n("+t+": "+l+")",n.name="ChunkLoadError",n.type=t,n.request=l,o[1](n)}}),"chunk-"+r,r)}},i.O.j=r=>0===e[r];var r=(r,s)=>{var o,t,[l,n,a]=s,c=0;if(l.some((r=>0!==e[r]))){for(o in n)i.o(n,o)&&(i.m[o]=n[o]);if(a)var d=a(i)}for(r&&r(s);c<l.length;c++)t=l[c],i.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return i.O(d)},s=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];s.forEach(r.bind(null,0)),s.push=r.bind(null,s.push.bind(s))})(),i.nc=void 0})();

File diff suppressed because one or more lines are too long

1
public/js/notifications-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/js/portfolio.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/post-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/profile-llsjbikoc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/rempos.js vendored

File diff suppressed because one or more lines are too long

2
public/js/rempro.js vendored

File diff suppressed because one or more lines are too long

2
public/js/search.js vendored

File diff suppressed because one or more lines are too long

2
public/js/spa.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,47 +1,49 @@
{
"/js/app.js": "/js/app.js?id=855c8b0847137dd56c72229ad305c032",
"/js/app.js": "/js/app.js?id=92f95bbb3a3996d97986f0bae1d19854",
"/js/activity.js": "/js/activity.js?id=d14e608dd1ecac4d900cd639f2199b62",
"/js/components.js": "/js/components.js?id=35beb0748dd5518371686a5f00348fbf",
"/js/components.js": "/js/components.js?id=a1d4c69ad0670101d7c449f31dfc0234",
"/js/discover.js": "/js/discover.js?id=0c98508635d6adae9bf8f76c9f5c918c",
"/js/profile.js": "/js/profile.js?id=a7773ad51508de1b07d3d27a0b750b2a",
"/js/status.js": "/js/status.js?id=9fd2af60a035dd86ff3cb31af111c3b7",
"/js/timeline.js": "/js/timeline.js?id=a2a547a3c177ae187ba420c21893e8e8",
"/js/compose.js": "/js/compose.js?id=61b03caa8ae7827b689847c19fc99212",
"/js/compose-classic.js": "/js/compose-classic.js?id=f11c60795e27213e446054259fdc8b50",
"/js/search.js": "/js/search.js?id=4bb81cba317cf1ad35f2c98dce78fd9d",
"/js/developers.js": "/js/developers.js?id=dd22facb8cf2746992404468a9373ac5",
"/js/hashtag.js": "/js/hashtag.js?id=5fe9d15d07a227f91eabd874cbb9fea2",
"/js/profile.js": "/js/profile.js?id=5b3fdd28701417e3b07c084ed4010dd1",
"/js/status.js": "/js/status.js?id=1256023f2252ea00bd413e8b88af7bba",
"/js/timeline.js": "/js/timeline.js?id=70f25266b6e023fce9241957604f2bec",
"/js/compose.js": "/js/compose.js?id=9e50779c7c795fb9e91442cc04fc0986",
"/js/compose-classic.js": "/js/compose-classic.js?id=bddb9f2188a782aa819d2e993f66132b",
"/js/search.js": "/js/search.js?id=5e0597874d446987159f185e3ac4a94f",
"/js/developers.js": "/js/developers.js?id=de7f83146a05d8cd9ef3d68de9e2b4f2",
"/js/hashtag.js": "/js/hashtag.js?id=45ba17ef807c13297eb6b05c00f5d04c",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=96e040f887859e77549afcadf3e0fdc9",
"/js/collections.js": "/js/collections.js?id=b7315297770a9c1b02cb64176158d2df",
"/js/collections.js": "/js/collections.js?id=c492e564ee9c7bb6fe1b4df71aa5912c",
"/js/profile-directory.js": "/js/profile-directory.js?id=04ec970031e6bf15de5ade019147d53e",
"/js/story-compose.js": "/js/story-compose.js?id=afe8f35cf52d92ac48ee68a9916d218d",
"/js/direct.js": "/js/direct.js?id=29127c125979e275afa50b47d692c892",
"/js/story-compose.js": "/js/story-compose.js?id=1ec3e09e9647176b6cb6db768a9d490b",
"/js/direct.js": "/js/direct.js?id=659767ee04e19dc4017f2cb358d57a69",
"/js/admin.js": "/js/admin.js?id=fd88b96423314b41cc763a0714554a04",
"/js/rempro.js": "/js/rempro.js?id=8ad8738264b7e0733f89ca605d6f347c",
"/js/rempos.js": "/js/rempos.js?id=47f6c3b3dc7954179a9e2024614449d4",
"/js/live-player.js": "/js/live-player.js?id=be9bb8d1d615e03356a7ea2a755dabd9",
"/js/spa.js": "/js/spa.js?id=c7903ea5481557c969ffc97999d64a12",
"/js/stories.js": "/js/stories.js?id=814a25875cac8987d85c801dcb453114",
"/js/installer.js": "/js/installer.js?id=d7b03f6c0bb707bec8ff9f81d328ac4a",
"/js/manifest.js": "/js/manifest.js?id=9ba42a85f6a0413c7493b02f749e3cc7",
"/js/home-ivl9d2teh.js": "/js/home-ivl9d2teh.js?id=a4f4874c61183b173479a9a67fa2e66f",
"/js/compose-ivl9d2teh.js": "/js/compose-ivl9d2teh.js?id=76a6e4e6eebeff5f9134db38263c6cd0",
"/js/post-ivl9d2teh.js": "/js/post-ivl9d2teh.js?id=de1b91878b05352f272dc2ab479d87b5",
"/js/profile-ivl9d2teh.js": "/js/profile-ivl9d2teh.js?id=a0b0d663e43431010fdaf446866106cf",
"/js/dmym-ivl9d2teh.js": "/js/dmym-ivl9d2teh.js?id=83dd473d9e9d005df20dc7973e4d3cf1",
"/js/dmyh-ivl9d2teh.js": "/js/dmyh-ivl9d2teh.js?id=e6bcbb23a10d6234ddfec0fcb21a6445",
"/js/daci-ivl9d2teh.js": "/js/daci-ivl9d2teh.js?id=5417060d4abef7ee44026081535509c8",
"/js/dffc-ivl9d2teh.js": "/js/dffc-ivl9d2teh.js?id=625d349892ac8e8ead5b65b7d20d4bca",
"/js/dsfc-ivl9d2teh.js": "/js/dsfc-ivl9d2teh.js?id=e948c7fe25618009af148cc765d3f829",
"/js/dssc-ivl9d2teh.js": "/js/dssc-ivl9d2teh.js?id=05be7e6a0bab04e8f23de3246dba5d50",
"/js/discover-ivl9d2teh.js": "/js/discover-ivl9d2teh.js?id=4e76e2d0b0bf815e1b23cb721d0ce8c4",
"/js/notifications-ivl9d2teh.js": "/js/notifications-ivl9d2teh.js?id=87e6ae02b7626c021e853d706920c647",
"/js/dms-ivl9d2teh.js": "/js/dms-ivl9d2teh.js?id=6068ce1647a66a8e9b50f2fbf4b7578e",
"/js/dmsg-ivl9d2teh.js": "/js/dmsg-ivl9d2teh.js?id=1a9976846519afea24cb9f316ab77e0e",
"/js/rempro.js": "/js/rempro.js?id=1eda2115dc663a8a1617329aba12bc66",
"/js/rempos.js": "/js/rempos.js?id=a2355bd9790509723a9b6efce4d33073",
"/js/live-player.js": "/js/live-player.js?id=a13ee6667e29c6159c5ba51be211dbaf",
"/js/spa.js": "/js/spa.js?id=8a1238ec25f4067b3436041eaf386fbb",
"/js/stories.js": "/js/stories.js?id=a4237af19ac2c4460ad34ecbb7bee7dd",
"/js/portfolio.js": "/js/portfolio.js?id=204f2ece254271935022cfb72c0042a1",
"/js/installer.js": "/js/installer.js?id=6df959ddb067a587dcc40a11909b9b1f",
"/js/manifest.js": "/js/manifest.js?id=cb3be33aeb160e02d6840c16f1fad134",
"/js/home-llsjbikoc.js": "/js/home-llsjbikoc.js?id=e10f6fadd6228067fccad09665999083",
"/js/compose-llsjbikoc.js": "/js/compose-llsjbikoc.js?id=68160b8601d8bf1267bbb9f0b5f61f6d",
"/js/post-llsjbikoc.js": "/js/post-llsjbikoc.js?id=c5a542c009aaea963c7487a07608db1e",
"/js/profile-llsjbikoc.js": "/js/profile-llsjbikoc.js?id=590c28a9a3c78c815f2d05202d86f00b",
"/js/dmym-llsjbikoc.js": "/js/dmym-llsjbikoc.js?id=2e66f1374e89f04f98c48ddbeee3ecba",
"/js/dmyh-llsjbikoc.js": "/js/dmyh-llsjbikoc.js?id=4951618f50447b7a6b83d8ca5a644278",
"/js/daci-llsjbikoc.js": "/js/daci-llsjbikoc.js?id=7eb70eb0ab1f82c9e9bcafc7bfe9bdfb",
"/js/dffc-llsjbikoc.js": "/js/dffc-llsjbikoc.js?id=9729f2c2f877c98ab561f01f34aa3d45",
"/js/dsfc-llsjbikoc.js": "/js/dsfc-llsjbikoc.js?id=a694c483689f1176fb7c43c6be735487",
"/js/dssc-llsjbikoc.js": "/js/dssc-llsjbikoc.js?id=7db93ebd0c628d659904dfde6e11631e",
"/js/discover-llsjbikoc.js": "/js/discover-llsjbikoc.js?id=93e22e635c1d67a41bfeba1cf1883ad4",
"/js/notifications-llsjbikoc.js": "/js/notifications-llsjbikoc.js?id=a96d4d13dcaf8c5a48d598756634718d",
"/js/dms-llsjbikoc.js": "/js/dms-llsjbikoc.js?id=d0cca63dce739a5e3129b7df3ffc3696",
"/js/dmsg-llsjbikoc.js": "/js/dmsg-llsjbikoc.js?id=a46273b401b145b2727616c4565a1e26",
"/css/appdark.css": "/css/appdark.css?id=aa186d0136f89d136461f0f5d84de682",
"/css/app.css": "/css/app.css?id=140a427d89c3aae4f78e87cf6ec7eef3",
"/css/spa.css": "/css/spa.css?id=4c78f163c6ad4e0f25ced75c7dd624b6",
"/css/portfolio.css": "/css/portfolio.css?id=b2e5ac36595185abfbeb0f9b114c2163",
"/css/admin.css": "/css/admin.css?id=3730bb9975e2a6ddc931f66dce1fcf49",
"/css/landing.css": "/css/landing.css?id=b488c3f0db85a50607d8ae12ac394a0f",
"/js/vendor.js": "/js/vendor.js?id=898bcd5e9be546c8b5699fe37805770e"
"/css/spa.css": "/css/spa.css?id=4c78f163c6ad4e0f25ced75c7dd624b6",
"/js/vendor.js": "/js/vendor.js?id=54cd37dcfb87097e5a701f4e4c3d9bc3"
}

View 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">&commat;{{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>

View 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>

View 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
View 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
View 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
View 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;
}

View file

@ -1,6 +1,6 @@
<nav class="navbar navbar-expand navbar-light navbar-laravel shadow-none border-bottom sticky-top py-1">
<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">
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config_cache('app.name') }}</span>
</a>

View 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

View 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

View 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>

View 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

View 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

View 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

View file

@ -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::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::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');
@ -352,6 +382,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('warning', 'AccountInterstitialController@read');
Route::get('my2020', 'SeasonalController@yearInReview');
Route::get('web/my-portfolio', 'PortfolioController@myRedirect');
Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect');
Route::get('web/username/{id}', 'SpaController@usernameRedirect');
Route::get('web/post/{id}', 'SpaController@webPost');