mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 06:21:27 +00:00
New landing page design
This commit is contained in:
parent
e7a19b2c18
commit
09c0032b39
16 changed files with 1779 additions and 126 deletions
45
app/Http/Controllers/LandingController.php
Normal file
45
app/Http/Controllers/LandingController.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Http\Resources\DirectoryProfile;
|
||||
|
||||
class LandingController extends Controller
|
||||
{
|
||||
public function directoryRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config_cache('landing.show_directory') != 1, 404);
|
||||
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function exploreRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config_cache('landing.show_explore_feed') != 1, 404);
|
||||
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function getDirectoryApi(Request $request)
|
||||
{
|
||||
abort_if(config_cache('landing.show_directory') != 1, 404);
|
||||
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
->whereIsSuggestable(true)
|
||||
->orderByDesc('updated_at')
|
||||
->cursorPaginate(20)
|
||||
);
|
||||
}
|
||||
}
|
37
app/Http/Resources/DirectoryProfile.php
Normal file
37
app/Http/Resources/DirectoryProfile.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Cache;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class DirectoryProfile extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$account = AccountService::get($this->id, true);
|
||||
if(!$account) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$url = url($this->username);
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'username' => $this->username,
|
||||
'url' => $url,
|
||||
'avatar' => $account['avatar'],
|
||||
'following_count' => $account['following_count'],
|
||||
'followers_count' => $account['followers_count'],
|
||||
'statuses_count' => $account['statuses_count'],
|
||||
'bio' => $account['note_text']
|
||||
];
|
||||
}
|
||||
}
|
104
app/Services/LandingService.php
Normal file
104
app/Services/LandingService.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class LandingService
|
||||
{
|
||||
public static function get($json = true)
|
||||
{
|
||||
$activeMonth = Cache::remember('api:nodeinfo:am', 172800, function() {
|
||||
return User::select('last_active_at')
|
||||
->where('last_active_at', '>', now()->subMonths(1))
|
||||
->orWhere('created_at', '>', now()->subMonths(1))
|
||||
->count();
|
||||
});
|
||||
|
||||
$totalUsers = Cache::remember('api:nodeinfo:users', 43200, function() {
|
||||
return User::count();
|
||||
});
|
||||
|
||||
$postCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
|
||||
return Status::whereLocal(true)->count();
|
||||
});
|
||||
|
||||
$contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
return $admin && isset($admin->profile_id) ?
|
||||
AccountService::getMastodon($admin->profile_id, true) :
|
||||
null;
|
||||
});
|
||||
|
||||
$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
|
||||
return config_cache('app.rules') ?
|
||||
collect(json_decode(config_cache('app.rules'), true))
|
||||
->map(function($rule, $key) {
|
||||
$id = $key + 1;
|
||||
return [
|
||||
'id' => "{$id}",
|
||||
'text' => $rule
|
||||
];
|
||||
})
|
||||
->toArray() : [];
|
||||
});
|
||||
|
||||
$res = [
|
||||
'name' => config_cache('app.name'),
|
||||
'url' => config_cache('app.url'),
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'show_directory' => config_cache('landing.show_directory') == 1,
|
||||
'show_explore_feed' => config_cache('landing.show_explore_feed') == 1,
|
||||
'open_registration' => config_cache('pixelfed.open_registration') == 1,
|
||||
'version' => config('pixelfed.version'),
|
||||
'about' => [
|
||||
'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'),
|
||||
'short_description' => config_cache('app.short_description'),
|
||||
'description' => config_cache('app.description'),
|
||||
],
|
||||
'stats' => [
|
||||
'active_users' => (int) $activeMonth,
|
||||
'posts_count' => (int) $postCount,
|
||||
'total_users' => (int) $totalUsers
|
||||
],
|
||||
'contact' => [
|
||||
'account' => $contactAccount,
|
||||
'email' => config('instance.email')
|
||||
],
|
||||
'rules' => $rules,
|
||||
'uploader' => [
|
||||
'max_photo_size' => (int) (config('pixelfed.max_photo_size') * 1024),
|
||||
'max_caption_length' => (int) config('pixelfed.max_caption_length'),
|
||||
'max_altext_length' => (int) config('pixelfed.max_altext_length', 150),
|
||||
'album_limit' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'image_quality' => (int) config_cache('pixelfed.image_quality'),
|
||||
'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
|
||||
'optimize_image' => (bool) config('pixelfed.optimize_image'),
|
||||
'optimize_video' => (bool) config('pixelfed.optimize_video'),
|
||||
'media_types' => config_cache('pixelfed.media_types'),
|
||||
],
|
||||
'features' => [
|
||||
'federation' => config_cache('federation.activitypub.enabled'),
|
||||
'timelines' => [
|
||||
'local' => true,
|
||||
'network' => (bool) config('federation.network_timeline'),
|
||||
],
|
||||
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
|
||||
'stories' => (bool) config_cache('instance.stories.enabled'),
|
||||
'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
|
||||
]
|
||||
];
|
||||
|
||||
if($json) {
|
||||
return json_encode($res);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
139
resources/assets/components/landing/Directory.vue
Normal file
139
resources/assets/components/landing/Directory.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<div class="landing-directory-component">
|
||||
<section class="page-wrapper">
|
||||
<div class="container container-compact">
|
||||
<div class="card bg-bluegray-900" style="border-radius: 10px;">
|
||||
<div class="card-header bg-bluegray-800 nav-menu" style="border-top-left-radius: 10px; border-top-right-radius: 10px;">
|
||||
<ul class="nav justify-content-around">
|
||||
<li class="nav-item">
|
||||
<router-link to="/" class="nav-link">About</router-link>
|
||||
</li>
|
||||
<li v-if="config.show_directory" class="nav-item">
|
||||
<router-link to="/web/directory" class="nav-link">Directory</router-link>
|
||||
</li>
|
||||
<li v-if="config.show_explore_feed" class="nav-item">
|
||||
<router-link to="/web/explore" class="nav-link">Explore</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="py-3">
|
||||
<p class="lead text-center">Discover accounts and people</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="d-flex justify-content-center align-items-center" style="min-height: 500px;">
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<div v-else class="feed-list">
|
||||
<user-card
|
||||
v-for="account in feed"
|
||||
:key="account.id"
|
||||
:account="account" />
|
||||
|
||||
<intersect v-if="canLoadMore && !isEmpty" @enter="enterIntersect">
|
||||
<div class="d-flex justify-content-center pt-5 pb-3">
|
||||
<b-spinner v-if="isLoadingMore" />
|
||||
</div>
|
||||
</intersect>
|
||||
</div>
|
||||
|
||||
<div v-if="isEmpty">
|
||||
<div class="card card-body bg-bluegray-800">
|
||||
<div class="d-flex justify-content-center align-items-center flex-column py-5">
|
||||
<i class="fal fa-clock fa-6x text-bluegray-500"></i>
|
||||
<p class="lead font-weight-bold mt-3 mb-0">Nothing to show yet! Check back later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer-component />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import UserCard from './partials/UserCard';
|
||||
import Intersect from 'vue-intersect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"user-card": UserCard,
|
||||
"intersect": Intersect,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
config: window.pfl,
|
||||
pagination: undefined,
|
||||
feed: [],
|
||||
isEmpty: false,
|
||||
canLoadMore: false,
|
||||
isIntersecting: false,
|
||||
isLoadingMore: false
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
if(this.config.show_directory == false) {
|
||||
this.$router.push('/');
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
axios.get('/api/landing/v1/directory')
|
||||
.then(res => {
|
||||
if(!res.data.data.length) {
|
||||
this.isEmpty = true;
|
||||
}
|
||||
this.feed = res.data.data;
|
||||
this.pagination = {...res.data.links, ...res.data.meta};
|
||||
})
|
||||
.finally(() => {
|
||||
this.canLoadMore = true;
|
||||
this.$nextTick(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
enterIntersect(e) {
|
||||
if(this.isIntersecting || !this.pagination.next_cursor) {
|
||||
return;
|
||||
}
|
||||
this.isIntersecting = true;
|
||||
this.isLoadingMore = true;
|
||||
|
||||
axios.get('/api/landing/v1/directory', {
|
||||
params: {
|
||||
cursor: this.pagination.next_cursor
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.feed.push(...res.data.data);
|
||||
this.pagination = {...res.data.links, ...res.data.meta};
|
||||
})
|
||||
.finally(() => {
|
||||
if(this.pagination.next_cursor) {
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.isLoadingMore = false;
|
||||
this.isIntersecting = false;
|
||||
});
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
119
resources/assets/components/landing/Explore.vue
Normal file
119
resources/assets/components/landing/Explore.vue
Normal file
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<div class="landing-explore-component">
|
||||
<section class="page-wrapper">
|
||||
<div class="container container-compact">
|
||||
<div class="card bg-bluegray-900" style="border-radius: 10px;">
|
||||
<div class="card-header bg-bluegray-800 nav-menu" style="border-top-left-radius: 10px; border-top-right-radius: 10px;">
|
||||
<ul class="nav justify-content-around">
|
||||
<li class="nav-item">
|
||||
<router-link to="/" class="nav-link">About</router-link>
|
||||
</li>
|
||||
<li v-if="config.show_directory" class="nav-item">
|
||||
<router-link to="/web/directory" class="nav-link">Directory</router-link>
|
||||
</li>
|
||||
<li v-if="config.show_explore_feed" class="nav-item">
|
||||
<router-link to="/web/explore" class="nav-link">Explore</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="py-3">
|
||||
<p class="lead text-center">Explore trending posts</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="d-flex justify-content-center align-items-center" style="min-height: 500px;">
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<div v-else class="feed-list">
|
||||
<post-card
|
||||
v-for="post in feed"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
:range="ranges[rangeIndex]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer-component />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import PostCard from './partials/PostCard';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"post-card": PostCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
config: window.pfl,
|
||||
isFetching: false,
|
||||
range: 'daily',
|
||||
ranges: ['daily', 'monthly', 'yearly'],
|
||||
rangeIndex: 0,
|
||||
feed: [],
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
if(this.config.show_explore_feed == false) {
|
||||
this.$router.push('/');
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
axios.get('/api/pixelfed/v2/discover/posts/trending?range=daily')
|
||||
.then(res => {
|
||||
if(res && res.data.length > 3) {
|
||||
this.feed = res.data;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.rangeIndex++;
|
||||
this.fetchTrending();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
fetchTrending() {
|
||||
if(this.isFetching || this.rangeIndex >= 3) {
|
||||
return;
|
||||
}
|
||||
this.isFetching = true;
|
||||
|
||||
axios.get('/api/pixelfed/v2/discover/posts/trending', {
|
||||
params: {
|
||||
range: this.ranges[this.rangeIndex]
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res && res.data.length) {
|
||||
if(this.rangeIndex == 2 && res.data.length > 3) {
|
||||
this.feed = res.data;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.rangeIndex++;
|
||||
this.isFetching = false;
|
||||
this.fetchTrending();
|
||||
}
|
||||
} else {
|
||||
this.rangeIndex++;
|
||||
this.isFetching = false;
|
||||
this.fetchTrending();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
231
resources/assets/components/landing/Index.vue
Normal file
231
resources/assets/components/landing/Index.vue
Normal file
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<div class="landing-index-component">
|
||||
<section class="page-wrapper">
|
||||
<div class="container container-compact">
|
||||
<div class="card bg-bluegray-900" style="border-radius: 10px;">
|
||||
<div class="card-header bg-bluegray-800 nav-menu" style="border-top-left-radius: 10px; border-top-right-radius: 10px;">
|
||||
<ul class="nav justify-content-around">
|
||||
<li class="nav-item">
|
||||
<router-link to="/" class="nav-link">About</router-link>
|
||||
</li>
|
||||
<li v-if="config.show_directory" class="nav-item">
|
||||
<router-link to="/web/directory" class="nav-link">Directory</router-link>
|
||||
</li>
|
||||
<li v-if="config.show_explore_feed" class="nav-item">
|
||||
<router-link to="/web/explore" class="nav-link">Explore</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-img-top p-2">
|
||||
<img
|
||||
:src="config.about.banner_image"
|
||||
class="img-fluid rounded"
|
||||
style="width: 100%;max-height: 200px;object-fit: cover;"
|
||||
alt="Server banner image"
|
||||
onerror="this.src='/storage/headers/default.jpg';this.onerror=null;">
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="server-header">
|
||||
<p class="server-header-domain">{{ config.domain }}</p>
|
||||
<p class="server-header-attribution">
|
||||
Decentralized photo sharing social media powered by <a href="https://pixelfed.org">Pixelfed</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="server-stats">
|
||||
<div class="list-group">
|
||||
<div class="list-group-item bg-transparent">
|
||||
<p class="stat-value">{{ formatCount(config.stats.posts_count) }}</p>
|
||||
<p class="stat-label">Posts</p>
|
||||
</div>
|
||||
<div class="list-group-item bg-transparent">
|
||||
<p class="stat-value">{{ formatCount(config.stats.active_users) }}</p>
|
||||
<p class="stat-label">Active Users</p>
|
||||
</div>
|
||||
<div class="list-group-item bg-transparent">
|
||||
<p class="stat-value">{{ formatCount(config.stats.total_users) }}</p>
|
||||
<p class="stat-label">Total Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="server-admin">
|
||||
<div class="list-group">
|
||||
<div v-if="config.contact.account" class="list-group-item bg-transparent">
|
||||
<p class="item-label">Managed By</p>
|
||||
<a :href="config.contact.account.url" class="admin-card">
|
||||
<div class="d-flex">
|
||||
<img :src="config.contact.account.avatar" onerror="this.src='/storage/avatars/default.jpg';this.onerror=null;" width="45" height="45" class="avatar">
|
||||
|
||||
<div class="user-info">
|
||||
<p class="display-name">{{ config.contact.account.display_name }}</p>
|
||||
<p class="username">@{{ config.contact.account.username }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="config.contact.email" class="list-group-item bg-transparent">
|
||||
<p class="item-label">Contact</p>
|
||||
<a :href="`mailto:${config.contact.email}`" class="admin-email">{{ config.contact.email }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="accordion">
|
||||
<div class="card bg-bluegray-700">
|
||||
<div class="card-header bg-bluegray-800" id="headingOne">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block" type="button" data-toggle="collapse" data-target="#collapseOne" aria-controls="collapseOne" @click="toggleAccordion(0)">
|
||||
<span class="text-white h5">
|
||||
<i class="far fa-info-circle mr-2 text-muted"></i>
|
||||
About
|
||||
</span>
|
||||
<i class="far" :class="[ accordionTab === 0 ? 'fa-chevron-left text-primary': 'fa-chevron-down']"></i>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="collapseOne" class="collapse" aria-labelledby="headingOne" data-parent="#accordion">
|
||||
<div class="card-body about-text">
|
||||
<p v-html="config.about.description"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-bluegray-700">
|
||||
<div class="card-header bg-bluegray-800" id="headingTwo">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo" @click="toggleAccordion(1)">
|
||||
<span class="text-white h5">
|
||||
<i class="far fa-list mr-2 text-muted"></i>
|
||||
Server Rules
|
||||
</span>
|
||||
<i class="far" :class="[ accordionTab === 1 ? 'fa-chevron-left text-primary': 'fa-chevron-down']"></i>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordion">
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-rules">
|
||||
<div v-for="rule in config.rules" class="list-group-item bg-bluegray-900">
|
||||
<div class="rule-id">{{ rule.id }}</div>
|
||||
<div class="rule-text">{{ rule.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-bluegray-700">
|
||||
<div class="card-header bg-bluegray-800" id="headingThree">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree" @click="toggleAccordion(2)">
|
||||
<span class="text-white h5">
|
||||
<i class="far fa-sparkles mr-2 text-muted"></i>
|
||||
Supported Features
|
||||
</span>
|
||||
<i class="far" :class="[ accordionTab === 2 ? 'fa-chevron-left text-primary': 'fa-chevron-down']"></i>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordion">
|
||||
<div class="card-body card-features">
|
||||
<div class="card-features-cloud">
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Photo Posts</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Photo Albums</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Photo Filters</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Collections</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Comments</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Hashtags</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Likes</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Notifications</div>
|
||||
<div class="badge badge-success"><i class="far fa-check-circle"></i> Shares</div>
|
||||
</div>
|
||||
|
||||
<div class="py-3">
|
||||
<p class="lead">
|
||||
<span>You can share up to <span class="font-weight-bold">{{ config.uploader.album_limit }}</span> photos*</span>
|
||||
<span v-if="config.features.video">or <span class="font-weight-bold">1</span> video*</span>
|
||||
<span>at a time with a max caption length of <span class="font-weight-bold">{{ config.uploader.max_caption_length }}</span> characters.</span>
|
||||
</p>
|
||||
<p class="small opacity-50">* - Maximum file size is {{ formatBytes(config.uploader.max_photo_size) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-features">
|
||||
<div class="list-group-item bg-bluegray-900">
|
||||
<div class="feature-label">Federation</div>
|
||||
<i class="far fa-lg" :class="[config.features.federation ? 'fa-check-circle' : 'fa-times-circle' ]"></i>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item bg-bluegray-900">
|
||||
<div class="feature-label">Mobile App Support</div>
|
||||
<i class="far fa-lg" :class="[config.features.mobile_apis ? 'fa-check-circle' : 'fa-times-circle' ]"></i>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item bg-bluegray-900">
|
||||
<div class="feature-label">Stories</div>
|
||||
<i class="far fa-lg" :class="[config.features.stories ? 'fa-check-circle' : 'fa-times-circle' ]"></i>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item bg-bluegray-900">
|
||||
<div class="feature-label">Videos</div>
|
||||
<i class="far fa-lg" :class="[config.features.video ? 'fa-check-circle' : 'fa-times-circle' ]"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer-component />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config: window.pfl,
|
||||
accordionTab: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleAccordion(idx) {
|
||||
if(this.accordionTab == idx) {
|
||||
this.accordionTab = undefined;
|
||||
return;
|
||||
}
|
||||
this.accordionTab = idx;
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
if(!val) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return val.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
|
||||
},
|
||||
|
||||
formatBytes(bytes, unit = 'megabyte') {
|
||||
const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
|
||||
const navigatorLocal = navigator.languages && navigator.languages.length >= 0 ? navigator.languages[0] : 'en-US';
|
||||
const unitIndex = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
|
||||
return Intl.NumberFormat(navigatorLocal, {
|
||||
style: 'unit',
|
||||
unit : units[unitIndex],
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
roundingMode: 'ceil'
|
||||
}).format(bytes / (1024 ** unitIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
14
resources/assets/components/landing/NotFound.vue
Normal file
14
resources/assets/components/landing/NotFound.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div class="landing-index-component h-100">
|
||||
<section class="page-wrapper h-100 d-flex flex-grow-1 justify-content-center align-items-center">
|
||||
<div class="d-flex flex-column align-items-center gap-3">
|
||||
<i class="fal fa-exclamation-triangle fa-5x text-bluegray-500"></i>
|
||||
<div class="text-center">
|
||||
<h2>404 - Not Found</h2>
|
||||
<p class="lead">The page you are looking for does not exist.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-light btn-lg rounded-pill px-4" href="/">Go back home</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
93
resources/assets/components/landing/partials/PostCard.vue
Normal file
93
resources/assets/components/landing/partials/PostCard.vue
Normal file
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div class="timeline-status-component">
|
||||
<div class="card bg-bluegray-800 landing-post-card" style="border-radius: 15px;">
|
||||
<div class="card-header border-0 bg-bluegray-700" style="border-top-left-radius: 15px;border-top-right-radius: 15px;">
|
||||
<div class="media align-items-center">
|
||||
<a :href="post.account.url" class="mr-2">
|
||||
<img :src="post.account.avatar" style="border-radius:30px;" width="30" height="30" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
|
||||
</a>
|
||||
|
||||
<div class="media-body d-flex justify-content-between align-items-center">
|
||||
<p class="font-weight-bold username mb-0">
|
||||
<a :href="post.account.url" class="text-white">@{{ post.account.username }}</a>
|
||||
</p>
|
||||
|
||||
<p class="font-weight-bold mb-0">
|
||||
<a v-if="range === 'daily'" :href="post.url" class="text-bluegray-500">Posted {{ timeago(post.created_at) }} ago</a>
|
||||
<a v-else :href="post.url" class="text-bluegray-400">View Post</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body m-0 p-0">
|
||||
<post-content :status="post" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import PostContent from './../../partials/post/PostContent';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'post',
|
||||
'range'
|
||||
],
|
||||
|
||||
components: {
|
||||
'post-content': PostContent,
|
||||
},
|
||||
|
||||
methods: {
|
||||
timestampToAgo(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 63072000);
|
||||
if (interval < 0) {
|
||||
return "0s";
|
||||
}
|
||||
if (interval >= 1) {
|
||||
return interval + "y";
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + "w";
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + "d";
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + "h";
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + "m";
|
||||
}
|
||||
return Math.floor(seconds) + "s";
|
||||
},
|
||||
|
||||
timeago(ts) {
|
||||
let short = this.timestampToAgo(ts);
|
||||
return short;
|
||||
if(
|
||||
short.endsWith('s') ||
|
||||
short.endsWith('m') ||
|
||||
short.endsWith('h')
|
||||
) {
|
||||
return short;
|
||||
}
|
||||
const intl = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
return intl.format(new Date(ts));
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
56
resources/assets/components/landing/partials/UserCard.vue
Normal file
56
resources/assets/components/landing/partials/UserCard.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<div class="card bg-bluegray-800 landing-user-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex" style="gap: 15px;">
|
||||
<div class="flex-shrink-1">
|
||||
<a :href="account.url">
|
||||
<img class="rounded-circle" :src="account.avatar" onerror="this.src='/storage/avatars/default.jpg';this.onerror=null;" width="50" height="50">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div v-if="account.name" class="display-name">
|
||||
<a :href="account.url">{{ account.name }}</a>
|
||||
</div>
|
||||
<p class="username">
|
||||
<a :href="account.url">@{{ account.username }}</a>
|
||||
</p>
|
||||
|
||||
<div class="user-stats">
|
||||
<div class="user-stats-item">{{ formatCount(account.statuses_count) }} Posts</div>
|
||||
<div class="user-stats-item">{{ formatCount(account.followers_count) }} Followers</div>
|
||||
<div class="user-stats-item">{{ formatCount(account.following_count) }} Following</div>
|
||||
</div>
|
||||
|
||||
<div v-if="account.bio" class="user-bio">
|
||||
<p class="small text-bluegray-400 mb-0">{{ truncate(account.bio) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: ['account'],
|
||||
|
||||
methods: {
|
||||
formatCount(val) {
|
||||
if(!val) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return val.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
|
||||
},
|
||||
|
||||
truncate(val, limit = 120) {
|
||||
if(!val || val.length < limit) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val.slice(0, limit) + '...'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
36
resources/assets/components/landing/sections/footer.vue
Normal file
36
resources/assets/components/landing/sections/footer.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div class="py-5">
|
||||
<p class="text-center text-uppercase font-weight-bold small text-justify">
|
||||
<a href="/site/help" class="text-bluegray-400 p-2">Help</a>
|
||||
<span class="mx-2 text-muted">·</span>
|
||||
<a href="/site/terms" class="text-bluegray-400 p-2">Terms</a>
|
||||
<span class="mx-2 text-muted">·</span>
|
||||
<a href="/site/privacy" class="text-bluegray-400 p-2">Privacy</a>
|
||||
<span class="mx-2 text-muted">·</span>
|
||||
<a href="https://pixelfed.org/mobile-apps" class="text-bluegray-400 p-2" target="_blank">Mobile Apps</a>
|
||||
</p>
|
||||
<p class="text-center text-bluegray-500 small mb-0">
|
||||
<span class="text-bluegray-500">© {{ getYear() }} {{config.domain}}</span>
|
||||
<span class="mx-2 text-muted">·</span>
|
||||
<a href="https://pixelfed.org" class="text-bluegray-500 font-weight-bold">Powered by Pixelfed</a>
|
||||
<span class="mx-2 text-muted">·</span>
|
||||
<span class="text-bluegray-500">v{{config.version}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config: window.pfl
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getYear() {
|
||||
return (new Date().getFullYear());
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
33
resources/assets/components/landing/sections/nav.vue
Normal file
33
resources/assets/components/landing/sections/nav.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark fixed-top">
|
||||
<div class="container" style="max-width: 600px;">
|
||||
<router-link to="/" class="navbar-brand">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Logo">
|
||||
<span class="mr-3">{{ name }}</span>
|
||||
</router-link>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
</ul>
|
||||
<div class="my-2 my-lg-0">
|
||||
<a class="btn btn-outline-light btn-sm rounded-pill font-weight-bold px-4" href="/login">Login</a>
|
||||
|
||||
<a v-if="config.open_registration" class="ml-2 btn btn-primary btn-primary-alt btn-sm rounded-pill font-weight-bold px-4" href="/register">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config: window.pfl,
|
||||
name: window.pfl.name,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
$(window).scroll(function(){
|
||||
$('nav').toggleClass('bg-black', $(this).scrollTop() > 20);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
295
resources/assets/js/landing.js
vendored
Normal file
295
resources/assets/js/landing.js
vendored
Normal file
|
@ -0,0 +1,295 @@
|
|||
require('./polyfill');
|
||||
import Vue from 'vue';
|
||||
window.Vue = Vue;
|
||||
import VueRouter from "vue-router";
|
||||
import Vuex from "vuex";
|
||||
import { sync } from "vuex-router-sync";
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
import Loading from 'vue-loading-overlay';
|
||||
import VueTimeago from 'vue-timeago';
|
||||
import VueCarousel from 'vue-carousel';
|
||||
import VueBlurHash from 'vue-blurhash';
|
||||
import VueMasonry from 'vue-masonry-css';
|
||||
import VueI18n from 'vue-i18n';
|
||||
window.pftxt = require('twitter-text');
|
||||
import 'vue-blurhash/dist/vue-blurhash.css'
|
||||
window.filesize = require('filesize');
|
||||
import swal from 'sweetalert';
|
||||
window._ = require('lodash');
|
||||
window.Popper = require('popper.js').default;
|
||||
window.pixelfed = window.pixelfed || {};
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
require('bootstrap');
|
||||
window.axios = require('axios');
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
require('readmore-js');
|
||||
window.blurhash = require("blurhash");
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
let token = document.head.querySelector('meta[name="csrf-token"]');
|
||||
if (token) {
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
||||
} else {
|
||||
console.error('CSRF token not found.');
|
||||
}
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueBlurHash);
|
||||
Vue.use(VueCarousel);
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(InfiniteLoading);
|
||||
Vue.use(Loading);
|
||||
Vue.use(VueMasonry);
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(VueTimeago, {
|
||||
name: 'Timeago',
|
||||
locale: 'en'
|
||||
});
|
||||
|
||||
Vue.component(
|
||||
'photo-presenter',
|
||||
require('./components/presenter/PhotoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-presenter',
|
||||
require('./components/presenter/VideoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'photo-album-presenter',
|
||||
require('./components/presenter/PhotoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-album-presenter',
|
||||
require('./components/presenter/VideoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'mixed-album-presenter',
|
||||
require('./components/presenter/MixedAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'navbar',
|
||||
require('./../components/landing/sections/nav.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'footer-component',
|
||||
require('./../components/landing/sections/footer.vue').default
|
||||
);
|
||||
|
||||
import IndexComponent from "./../components/landing/Index.vue";
|
||||
import DirectoryComponent from "./../components/landing/Directory.vue";
|
||||
import ExploreComponent from "./../components/landing/Explore.vue";
|
||||
import NotFoundComponent from "./../components/landing/NotFound.vue";
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
linkActiveClass: "",
|
||||
linkExactActiveClass: "active",
|
||||
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
component: IndexComponent
|
||||
},
|
||||
{
|
||||
path: "/web/directory",
|
||||
component: DirectoryComponent
|
||||
},
|
||||
{
|
||||
path: "/web/explore",
|
||||
component: ExploreComponent
|
||||
},
|
||||
{
|
||||
path: "/*",
|
||||
component: NotFoundComponent,
|
||||
props: true
|
||||
},
|
||||
],
|
||||
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (to.hash) {
|
||||
return {
|
||||
selector: `[id='${to.hash.slice(1)}']`
|
||||
};
|
||||
} else {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function lss(name, def) {
|
||||
let key = 'pf_m2s.' + name;
|
||||
let ls = window.localStorage;
|
||||
if(ls.getItem(key)) {
|
||||
let val = ls.getItem(key);
|
||||
if(['pl', 'color-scheme'].includes(name)) {
|
||||
return val;
|
||||
}
|
||||
return ['true', true].includes(val);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
version: 1,
|
||||
hideCounts: true,
|
||||
autoloadComments: false,
|
||||
newReactions: false,
|
||||
fixedHeight: false,
|
||||
profileLayout: 'grid',
|
||||
showDMPrivacyWarning: true,
|
||||
relationships: {},
|
||||
emoji: [],
|
||||
colorScheme: lss('color-scheme', 'system'),
|
||||
},
|
||||
|
||||
getters: {
|
||||
getVersion: state => {
|
||||
return state.version;
|
||||
},
|
||||
|
||||
getHideCounts: state => {
|
||||
return state.hideCounts;
|
||||
},
|
||||
|
||||
getAutoloadComments: state => {
|
||||
return state.autoloadComments;
|
||||
},
|
||||
|
||||
getNewReactions: state => {
|
||||
return state.newReactions;
|
||||
},
|
||||
|
||||
getFixedHeight: state => {
|
||||
return state.fixedHeight;
|
||||
},
|
||||
|
||||
getProfileLayout: state => {
|
||||
return state.profileLayout;
|
||||
},
|
||||
|
||||
getRelationship: (state) => (id) => {
|
||||
return state.relationships[id];
|
||||
},
|
||||
|
||||
getCustomEmoji: state => {
|
||||
return state.emoji;
|
||||
},
|
||||
|
||||
getColorScheme: state => {
|
||||
return state.colorScheme;
|
||||
},
|
||||
|
||||
getShowDMPrivacyWarning: state => {
|
||||
return state.showDMPrivacyWarning;
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setVersion(state, value) {
|
||||
state.version = value;
|
||||
},
|
||||
|
||||
setHideCounts(state, value) {
|
||||
localStorage.setItem('pf_m2s.hc', value);
|
||||
state.hideCounts = value;
|
||||
},
|
||||
|
||||
setAutoloadComments(state, value) {
|
||||
localStorage.setItem('pf_m2s.ac', value);
|
||||
state.autoloadComments = value;
|
||||
},
|
||||
|
||||
setNewReactions(state, value) {
|
||||
localStorage.setItem('pf_m2s.nr', value);
|
||||
state.newReactions = value;
|
||||
},
|
||||
|
||||
setFixedHeight(state, value) {
|
||||
localStorage.setItem('pf_m2s.fh', value);
|
||||
state.fixedHeight = value;
|
||||
},
|
||||
|
||||
setProfileLayout(state, value) {
|
||||
localStorage.setItem('pf_m2s.pl', value);
|
||||
state.profileLayout = value;
|
||||
},
|
||||
|
||||
updateRelationship(state, relationships) {
|
||||
relationships.forEach((relationship) => {
|
||||
Vue.set(state.relationships, relationship.id, relationship)
|
||||
})
|
||||
},
|
||||
|
||||
updateCustomEmoji(state, emojis) {
|
||||
state.emoji = emojis;
|
||||
},
|
||||
|
||||
setColorScheme(state, value) {
|
||||
if(state.colorScheme == value) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('pf_m2s.color-scheme', value);
|
||||
state.colorScheme = value;
|
||||
const name = value == 'system' ? '' : (value == 'light' ? 'force-light-mode' : 'force-dark-mode');
|
||||
document.querySelector("body").className = name;
|
||||
if(name != 'system') {
|
||||
const payload = name == 'force-dark-mode' ? { dark_mode: 'on' } : {};
|
||||
axios.post('/settings/labs', payload);
|
||||
}
|
||||
},
|
||||
|
||||
setShowDMPrivacyWarning(state, value) {
|
||||
localStorage.setItem('pf_m2s.dmpwarn', value);
|
||||
state.showDMPrivacyWarning = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let i18nMessages = {
|
||||
en: require('./i18n/en.json'),
|
||||
ar: require('./i18n/ar.json'),
|
||||
ca: require('./i18n/ca.json'),
|
||||
de: require('./i18n/de.json'),
|
||||
el: require('./i18n/el.json'),
|
||||
es: require('./i18n/es.json'),
|
||||
eu: require('./i18n/eu.json'),
|
||||
fr: require('./i18n/fr.json'),
|
||||
he: require('./i18n/he.json'),
|
||||
gd: require('./i18n/gd.json'),
|
||||
gl: require('./i18n/gl.json'),
|
||||
id: require('./i18n/id.json'),
|
||||
it: require('./i18n/it.json'),
|
||||
ja: require('./i18n/ja.json'),
|
||||
nl: require('./i18n/nl.json'),
|
||||
pl: require('./i18n/pl.json'),
|
||||
pt: require('./i18n/pt.json'),
|
||||
ru: require('./i18n/ru.json'),
|
||||
uk: require('./i18n/uk.json'),
|
||||
vi: require('./i18n/vi.json'),
|
||||
};
|
||||
|
||||
let locale = document.querySelector('html').getAttribute('lang');
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: locale, // set locale
|
||||
fallbackLocale: 'en',
|
||||
messages: i18nMessages
|
||||
});
|
||||
|
||||
sync(store, router);
|
||||
|
||||
const App = new Vue({
|
||||
el: '#content',
|
||||
i18n,
|
||||
router,
|
||||
store
|
||||
});
|
552
resources/assets/sass/landing.scss
vendored
552
resources/assets/sass/landing.scss
vendored
|
@ -2,12 +2,556 @@
|
|||
|
||||
@import "fonts";
|
||||
@import "lib/fontawesome";
|
||||
@import "lib/inter";
|
||||
@import "lib/manrope";
|
||||
@import 'variables';
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import 'custom';
|
||||
|
||||
.container.slim {
|
||||
width: auto;
|
||||
max-width: 680px;
|
||||
padding: 0 15px;
|
||||
body {
|
||||
background: #080e2b;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
background-color: #080e2b;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
&-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.nav-link {
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bg-bluegray {
|
||||
&-700 {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
&-800 {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
&-900 {
|
||||
background-color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
.text-bluegray {
|
||||
&-400 {
|
||||
color: #94a3b8;
|
||||
}
|
||||
&-500 {
|
||||
color: #64748b;
|
||||
}
|
||||
&-600 {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
position: relative;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
min-height: 100vh !important;
|
||||
background-color: #212529 !important;
|
||||
background-image: url("/_landing/bg.jpg");
|
||||
background-size: cover !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
|
||||
.container-compact {
|
||||
max-width: 600px;
|
||||
margin-top: 3rem;
|
||||
padding-top: 3rem;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.bg-glass {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.text-gradient-primary {
|
||||
background: linear-gradient(to right, #6366f1, #8B5CF6, #D946EF);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.btn-primary-alt {
|
||||
border: none;
|
||||
outline: none;
|
||||
color: white;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
text-shadow: 3px 3px 10px rgba(0,0,0,.45);
|
||||
&:before, &:after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border-radius: 10em;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 105%;
|
||||
height: 105%;
|
||||
content: '';
|
||||
z-index: -2;
|
||||
background-size: 400% 400%;
|
||||
background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
|
||||
}
|
||||
&:before {
|
||||
filter: blur(7px);
|
||||
transition: all .25s ease;
|
||||
animation: pulse 10s infinite ease;
|
||||
}
|
||||
&:after {
|
||||
filter: blur(0.3px);
|
||||
}
|
||||
&:hover {
|
||||
&:before {
|
||||
width: 115%;
|
||||
height: 115%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.opacity-30 {
|
||||
opacity: 30%;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
border-bottom: 1px solid #334155;
|
||||
.nav-link {
|
||||
color: #94a3b8;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
|
||||
@media(min-width: 768px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.active:before,
|
||||
&.active:after,
|
||||
&.nav-item:hover:before,
|
||||
&.nav-item:hover:after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
border: solid 10px transparent;
|
||||
border-bottom: solid 0px transparent;
|
||||
border-width: 10px;
|
||||
bottom: -12px;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
border-color: transparent transparent #334155;
|
||||
}
|
||||
|
||||
&.active:after,
|
||||
&.nav-item:hover:after {
|
||||
bottom: -14px;
|
||||
border-color: transparent transparent #0f172a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.landing-index-component {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.logo {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--light);
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.server-header {
|
||||
margin: 0 0 30px 0;
|
||||
|
||||
&-domain {
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&-attribution {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.6px;
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-stats {
|
||||
margin: 30px 0;
|
||||
|
||||
.list-group {
|
||||
flex-direction: column;
|
||||
border-color: #1e293b;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
|
||||
&-item {
|
||||
border-color: #1e293b;
|
||||
flex-grow: 1;
|
||||
border-top-width: 1px;
|
||||
border-left-width: 0;
|
||||
|
||||
&:first-child {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.25rem;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
border-color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
margin-bottom: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
}
|
||||
|
||||
.server-admin {
|
||||
margin: 30px 0;
|
||||
|
||||
.list-group {
|
||||
flex-direction: column;
|
||||
border-color: #1e293b;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
|
||||
&-item {
|
||||
border-color: #1e293b;
|
||||
flex-grow: 1;
|
||||
border-top-width: 1px;
|
||||
border-left-width: 0;
|
||||
|
||||
&:first-child {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.25rem;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
border-color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.item-label {
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.admin-card {
|
||||
text-decoration: none;
|
||||
|
||||
.d-flex {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.display-name {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.display-name,
|
||||
.username {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-email {
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion {
|
||||
.btn-block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
|
||||
.h5 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.far {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
.far {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-text {
|
||||
padding: 40px 24px;
|
||||
|
||||
p {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-rules {
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border-color: #475569;
|
||||
|
||||
.rule-id {
|
||||
color: #475569;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.rule-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-features {
|
||||
&-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 10px 5px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.badge {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
padding: 5px 10px;
|
||||
|
||||
&-success {
|
||||
background: #86efac30;
|
||||
}
|
||||
|
||||
.far {
|
||||
margin-right: 5px;
|
||||
color: #22c55e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-features {
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-color: #475569;
|
||||
|
||||
.feature-label {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.fa-times-circle {
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.fa-check-circle {
|
||||
color: #22c55e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.landing-directory-component {
|
||||
.feed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.landing-user-card {
|
||||
.display-name {
|
||||
a {
|
||||
@extend .text-bluegray-400;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: 2px;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&-item {
|
||||
@extend .text-bluegray-500;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.user-bio {
|
||||
@extend .bg-bluegray-700;
|
||||
margin-top: 1rem;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.landing-explore-component {
|
||||
.feed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.landing-post-card {
|
||||
a.text-bluegray-400 {
|
||||
&:hover {
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.text-bluegray-500 {
|
||||
&:hover {
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.read-more-component {
|
||||
color: #64748b;
|
||||
a {
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
<!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">
|
||||
|
@ -9,7 +8,7 @@
|
|||
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
<title>{{ config('app.name', 'Pixelfed') }}</title>
|
||||
|
||||
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
|
||||
<meta property="og:title" content="{{ config('app.name', 'pixelfed') }}">
|
||||
|
@ -24,125 +23,27 @@
|
|||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.feature-circle {
|
||||
display: flex !important;
|
||||
-webkit-box-pack: center !important;
|
||||
justify-content: center !important;
|
||||
-webkit-box-align: center !important;
|
||||
align-items: center !important;
|
||||
margin-right: 1rem !important;
|
||||
background-color: #08d !important;
|
||||
color: #fff;
|
||||
border-radius: 50% !important;
|
||||
width: 60px;
|
||||
height:60px;
|
||||
}
|
||||
.section-spacer {
|
||||
height: 13vh;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
window.pfl = {!! App\Services\LandingService::get() !!}
|
||||
</script>
|
||||
</head>
|
||||
<body class="">
|
||||
<body>
|
||||
<main id="content">
|
||||
<section class="container">
|
||||
<div class="section-spacer"></div>
|
||||
<div class="row pt-md-5 mt-5">
|
||||
<div class="col-12 col-md-6 d-none d-md-block">
|
||||
<div class="m-my-4">
|
||||
<p class="display-2 font-weight-bold">Photo Sharing</p>
|
||||
<p class="h1 font-weight-bold">For Everyone.</p>
|
||||
</div>
|
||||
|
||||
<p class="lead font-weight-light mt-5">{{ config_cache('app.short_description') ?? 'Pixelfed is an image sharing platform, an ethical alternative to centralized platforms.' }}</p>
|
||||
<p><a href="https://pixelfed.org" target="_blank" class="font-weight-bold">Learn more</a></p>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 offset-md-1">
|
||||
<div>
|
||||
<div class="pt-md-3 d-flex justify-content-center align-items-center">
|
||||
<img src="/img/pixelfed-icon-color.svg" loading="lazy" width="50px" height="50px">
|
||||
<span class="font-weight-bold h3 ml-2 pt-2">{{ config_cache('app.name') ?? 'Pixelfed' }}</span>
|
||||
</div>
|
||||
<div class="d-block d-md-none">
|
||||
<p class="font-weight-light mt-3 mb-5 text-center px-5">{{ config_cache('app.short_description') ?? 'Pixelfed is an image sharing platform, an ethical alternative to centralized platforms.' }}</p>
|
||||
</div>
|
||||
<div class="card my-4 shadow-none border">
|
||||
<div class="card-body px-lg-5">
|
||||
<div class="text-center">
|
||||
<p class="small text-uppercase font-weight-bold text-muted">Account Login</p>
|
||||
</div>
|
||||
<div>
|
||||
<form class="px-1" method="POST" action="{{ route('login') }}" id="login_form">
|
||||
@csrf
|
||||
<div class="form-group row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" placeholder="{{__('Email')}}" required autofocus>
|
||||
|
||||
@if ($errors->has('email'))
|
||||
<span class="invalid-feedback">
|
||||
<strong>{{ $errors->first('email') }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{__('Password')}}" required>
|
||||
|
||||
@if ($errors->has('password'))
|
||||
<span class="invalid-feedback">
|
||||
<strong>{{ $errors->first('password') }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
<span class="font-weight-bold small ml-1 text-muted">
|
||||
{{ __('Remember Me') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if(config('captcha.enabled'))
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
{!! Captcha::display() !!}
|
||||
</div>
|
||||
@endif
|
||||
<div class="form-group row mb-0">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg font-weight-bold text-uppercase">
|
||||
{{ __('Login') }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-none border card-body">
|
||||
<p class="text-center mb-0 font-weight-bold">
|
||||
@if(config_cache('pixelfed.open_registration'))
|
||||
<a href="/register">Register</a>
|
||||
<span class="px-1">·</span>
|
||||
@endif
|
||||
<a href="/password/reset">Password Reset</a>
|
||||
<noscript>
|
||||
<div class="container">
|
||||
<h1 class="pt-5 text-center">Pixelfed</h1>
|
||||
<p class="text-center">Decentralized photo sharing social media</p>
|
||||
<p class="pt-2 text-center lead">
|
||||
<a href="{{ config('app.url') }}/login" class="btn btn-outline-light">Login</a>
|
||||
</p>
|
||||
<p class="pt-2 text-center lead">Please enable javascript to view this content.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</noscript>
|
||||
<navbar></navbar>
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
@include('layouts.partial.footer')
|
||||
<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/landing.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -214,4 +214,8 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
|
|||
Route::post('instances/moderate', 'Api\AdminApiController@moderateInstance')->middleware($middleware);
|
||||
Route::post('instances/refresh-stats', 'Api\AdminApiController@refreshInstanceStats')->middleware($middleware);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'landing/v1'], function() use($middleware) {
|
||||
Route::get('directory', 'LandingController@getDirectoryApi');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -150,6 +150,8 @@ Route::domain(config('portfolio.domain'))->group(function () {
|
|||
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
|
||||
Route::get('/', 'SiteController@home')->name('timeline.personal');
|
||||
Route::redirect('/home', '/')->name('home');
|
||||
Route::get('web/directory', 'LandingController@directoryRedirect');
|
||||
Route::get('web/explore', 'LandingController@exploreRedirect');
|
||||
|
||||
Auth::routes();
|
||||
|
||||
|
|
Loading…
Reference in a new issue