diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ece6c695..20a9a4bca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Added
- New media:fix-nonlocal-driver command. Fixes s3 media created with invalid FILESYSTEM_DRIVER=s3 configuration ([672cccd4](https://github.com/pixelfed/pixelfed/commit/672cccd4))
+- New landing page design ([09c0032b](https://github.com/pixelfed/pixelfed/commit/09c0032b))
### Updates
- Update ApiV1Controller, fix blocking remote accounts. Closes #4256 ([8e71e0c0](https://github.com/pixelfed/pixelfed/commit/8e71e0c0))
diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php
index 48b920dba..ab1a4ab5c 100644
--- a/app/Http/Controllers/Admin/AdminSettingsController.php
+++ b/app/Http/Controllers/Admin/AdminSettingsController.php
@@ -140,7 +140,9 @@ trait AdminSettingsController
'show_custom_css' => 'uikit.show_custom.css',
'show_custom_js' => 'uikit.show_custom.js',
'cloud_storage' => 'pixelfed.cloud_storage',
- 'account_autofollow' => 'account.autofollow'
+ 'account_autofollow' => 'account.autofollow',
+ 'show_directory' => 'landing.show_directory',
+ 'show_explore_feed' => 'landing.show_explore_feed',
];
foreach ($bools as $key => $value) {
diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php
new file mode 100644
index 000000000..9d5d50d53
--- /dev/null
+++ b/app/Http/Controllers/LandingController.php
@@ -0,0 +1,45 @@
+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)
+ );
+ }
+}
diff --git a/app/Http/Resources/DirectoryProfile.php b/app/Http/Resources/DirectoryProfile.php
new file mode 100644
index 000000000..d5d4b5841
--- /dev/null
+++ b/app/Http/Resources/DirectoryProfile.php
@@ -0,0 +1,37 @@
+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']
+ ];
+ }
+}
diff --git a/app/Services/LandingService.php b/app/Services/LandingService.php
new file mode 100644
index 000000000..a59e91e80
--- /dev/null
+++ b/app/Services/LandingService.php
@@ -0,0 +1,104 @@
+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;
+ }
+}
diff --git a/public/_landing/bg.jpg b/public/_landing/bg.jpg
new file mode 100644
index 000000000..46b3fb1da
Binary files /dev/null and b/public/_landing/bg.jpg differ
diff --git a/public/css/landing.css b/public/css/landing.css
index 0160440cd..5cfc430f2 100644
Binary files a/public/css/landing.css and b/public/css/landing.css differ
diff --git a/public/fonts/Manrope-Bold.woff b/public/fonts/Manrope-Bold.woff
new file mode 100644
index 000000000..469401f59
Binary files /dev/null and b/public/fonts/Manrope-Bold.woff differ
diff --git a/public/fonts/Manrope-ExtraLight.woff b/public/fonts/Manrope-ExtraLight.woff
new file mode 100644
index 000000000..96c01cd24
Binary files /dev/null and b/public/fonts/Manrope-ExtraLight.woff differ
diff --git a/public/fonts/Manrope-Regular.woff b/public/fonts/Manrope-Regular.woff
new file mode 100644
index 000000000..1f80f6c49
Binary files /dev/null and b/public/fonts/Manrope-Regular.woff differ
diff --git a/public/js/app.js b/public/js/app.js
index 2480c310d..86bae4e30 100644
Binary files a/public/js/app.js and b/public/js/app.js differ
diff --git a/public/js/landing.js b/public/js/landing.js
new file mode 100644
index 000000000..0feb25b13
Binary files /dev/null and b/public/js/landing.js differ
diff --git a/public/js/landing.js.LICENSE.txt b/public/js/landing.js.LICENSE.txt
new file mode 100644
index 000000000..ba8e2aeda
--- /dev/null
+++ b/public/js/landing.js.LICENSE.txt
@@ -0,0 +1 @@
+/*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */
diff --git a/public/js/spa.js b/public/js/spa.js
index 7eb9a75f4..d9f342348 100644
Binary files a/public/js/spa.js and b/public/js/spa.js differ
diff --git a/public/mix-manifest.json b/public/mix-manifest.json
index 32b3ce0eb..066b4cafd 100644
Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ
diff --git a/resources/assets/components/landing/Directory.vue b/resources/assets/components/landing/Directory.vue
new file mode 100644
index 000000000..c8a1397b3
--- /dev/null
+++ b/resources/assets/components/landing/Directory.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
Discover accounts and people
+
+
+
+
+
+
+
+
+
+
+
+
+
Nothing to show yet! Check back later.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/landing/Explore.vue b/resources/assets/components/landing/Explore.vue
new file mode 100644
index 000000000..16152e7fc
--- /dev/null
+++ b/resources/assets/components/landing/Explore.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
Explore trending posts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/landing/Index.vue b/resources/assets/components/landing/Index.vue
new file mode 100644
index 000000000..09bf62d00
--- /dev/null
+++ b/resources/assets/components/landing/Index.vue
@@ -0,0 +1,239 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ formatCount(config.stats.posts_count) }}
+
Posts
+
+
+
{{ formatCount(config.stats.active_users) }}
+
Active Users
+
+
+
{{ formatCount(config.stats.total_users) }}
+
Total Users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ rule.id }}
+
{{ rule.text }}
+
+
+
+
+
+
+
+
+
+
+
+
Photo Posts
+
Photo Albums
+
Photo Filters
+
Collections
+
Comments
+
Hashtags
+
Likes
+
Notifications
+
Shares
+
+
+
+
+ You can share up to {{ config.uploader.album_limit }} photos*
+ or 1 video*
+ at a time with a max caption length of {{ config.uploader.max_caption_length }} characters.
+
+
* - Maximum file size is {{ formatBytes(config.uploader.max_photo_size) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/landing/NotFound.vue b/resources/assets/components/landing/NotFound.vue
new file mode 100644
index 000000000..036aed662
--- /dev/null
+++ b/resources/assets/components/landing/NotFound.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
404 - Not Found
+
The page you are looking for does not exist.
+
+
Go back home
+
+
+
+
diff --git a/resources/assets/components/landing/partials/PostCard.vue b/resources/assets/components/landing/partials/PostCard.vue
new file mode 100644
index 000000000..84b77c93a
--- /dev/null
+++ b/resources/assets/components/landing/partials/PostCard.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
diff --git a/resources/assets/components/landing/partials/UserCard.vue b/resources/assets/components/landing/partials/UserCard.vue
new file mode 100644
index 000000000..6ef64ef2b
--- /dev/null
+++ b/resources/assets/components/landing/partials/UserCard.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+ @{{ account.username }}
+
+
+
+
{{ formatCount(account.statuses_count) }} Posts
+
{{ formatCount(account.followers_count) }} Followers
+
{{ formatCount(account.following_count) }} Following
+
+
+
+
{{ truncate(account.bio) }}
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/landing/sections/footer.vue b/resources/assets/components/landing/sections/footer.vue
new file mode 100644
index 000000000..8733030e4
--- /dev/null
+++ b/resources/assets/components/landing/sections/footer.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/resources/assets/components/landing/sections/nav.vue b/resources/assets/components/landing/sections/nav.vue
new file mode 100644
index 000000000..38fde6ef2
--- /dev/null
+++ b/resources/assets/components/landing/sections/nav.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
diff --git a/resources/assets/js/landing.js b/resources/assets/js/landing.js
new file mode 100644
index 000000000..6d903bcb5
--- /dev/null
+++ b/resources/assets/js/landing.js
@@ -0,0 +1,293 @@
+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 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(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
+});
diff --git a/resources/assets/sass/landing.scss b/resources/assets/sass/landing.scss
index e07472856..11857186a 100644
--- a/resources/assets/sass/landing.scss
+++ b/resources/assets/sass/landing.scss
@@ -2,12 +2,625 @@
@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;
-}
\ No newline at end of file
+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;
+ }
+ }
+}
+
+.footer-component {
+ padding: 3rem 1rem 1rem 1rem;
+
+ &-links {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: center;
+ margin-bottom: 3rem;
+ font-size: 15px;
+
+ a {
+ color: #94a3b8;
+ font-weight: 700;
+ text-transform: uppercase;
+ }
+
+ .spacer {
+ display: none;
+
+ @include media-breakpoint-up(md) {
+ color: #64748b;
+ display: block !important;
+ }
+ }
+ }
+
+ &-attribution {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ align-items: center;
+ color: #64748b;
+ font-size: 13px;
+
+ a {
+ color: #64748b;
+ font-weight: 700;
+ }
+
+ .spacer {
+ display: none;
+
+ @include media-breakpoint-up(md) {
+ color: #64748b;
+ display: block !important;
+ }
+ }
+ }
+
+ @include media-breakpoint-up(md) {
+ padding: 3rem 0;
+
+ &-links {
+ margin-bottom: 1rem;
+ flex-direction: row;
+ justify-content: center;
+ font-size: 13px;
+ }
+
+ &-attribution {
+ flex-direction: row;
+ justify-content: center;
+ font-size: 11.5px;
+ }
+ }
+
+}
+
+.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: #94a3b8;
+ 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: #94a3b8;
+ 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;
+ }
+ }
+ }
+
+ }
+}
diff --git a/resources/views/admin/settings/home.blade.php b/resources/views/admin/settings/home.blade.php
index a9835bf7d..82e743685 100644
--- a/resources/views/admin/settings/home.blade.php
+++ b/resources/views/admin/settings/home.blade.php
@@ -12,6 +12,9 @@
+
+ Landing
+
Brand
@@ -142,6 +145,42 @@
--}}
+
+