Merge pull request #1104 from pixelfed/frontend-ui-refactor

Frontend ui refactor
This commit is contained in:
daniel 2019-04-02 23:44:06 -06:00 committed by GitHub
commit 82102376fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1439 additions and 3186 deletions

View file

@ -9,6 +9,7 @@ use Cache;
use App\Comment;
use App\Jobs\CommentPipeline\CommentPipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Util\Lexer\Autolink;
use App\Profile;
use App\Status;
use League\Fractal;
@ -53,10 +54,11 @@ class CommentController extends Controller
Cache::forget('transform:status:'.$status->url());
$autolink = Autolink::create()->autolink($comment);
$reply = new Status();
$reply->profile_id = $profile->id;
$reply->caption = e($comment);
$reply->rendered = $comment;
$reply->rendered = $autolink;
$reply->in_reply_to_id = $status->id;
$reply->in_reply_to_profile_id = $status->profile_id;
$reply->save();

View file

@ -63,6 +63,11 @@ class FederationController extends Controller
$follower = Auth::user()->profile;
$url = $request->input('url');
$url = Helpers::validateUrl($url);
if(!$url) {
return;
}
RemoteFollowPipeline::dispatch($follower, $url);

View file

@ -9,6 +9,11 @@ use App\Status;
use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache;
use App\Transformer\Api\{
AccountTransformer,
HashtagTransformer,
StatusTransformer,
};
class SearchController extends Controller
{
@ -22,26 +27,33 @@ class SearchController extends Controller
if(mb_strlen($tag) < 3) {
return;
}
$tag = e(urldecode($tag));
$hash = hash('sha256', $tag);
$tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
$tokens = collect([]);
if(Helpers::validateUrl($tag)) {
$tokens = [];
if(Helpers::validateUrl($tag) != false) {
$remote = Helpers::fetchFromUrl($tag);
if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) {
$type = $remote['type'];
if($type == 'Person') {
$item = Helpers::profileFirstOrNew($tag);
$tokens->push([[
$tokens['profiles'] = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
]]);
'entity' => [
'id' => $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'thumb' => $item->avatarUrl()
]
]];
} else if ($type == 'Create') {
$item = Helpers::statusFirstOrFetch($tag, false);
$tokens->push([[
$tokens['posts'] = [[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
@ -49,10 +61,9 @@ class SearchController extends Controller
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]]);
]];
}
}
}
$hashtags = Hashtag::select('id', 'name', 'slug')->where('slug', 'like', '%'.$tag.'%')->whereHas('posts')->limit(20)->get();
if($hashtags->count() > 0) {
@ -62,17 +73,19 @@ class SearchController extends Controller
'url' => $item->url(),
'type' => 'hashtag',
'value' => $item->name,
'tokens' => explode('-', $item->name),
'tokens' => '',
'name' => null,
];
});
$tokens->push($tags);
$tokens['hashtags'] = $tags;
}
return $tokens;
});
$users = Profile::select('username', 'name', 'id')
->whereNull('status')
->whereNull('domain')
->where('id', '!=', Auth::user()->profile->id)
->where('username', 'like', '%'.$tag.'%')
//->orWhere('remote_url', $tag)
->orWhere('remote_url', $tag)
->limit(20)
->get();
@ -85,14 +98,21 @@ class SearchController extends Controller
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'id' => $item->id
'avatar' => $item->avatarUrl(),
'id' => $item->id,
'entity' => [
'id' => $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'thumb' => $item->avatarUrl()
]
];
});
$tokens->push($profiles);
if(isset($tokens['profiles'])) {
array_push($tokens['profiles'], $profiles);
} else {
$tokens['profiles'] = $profiles;
}
}
return $tokens;
});
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media')
->whereNull('in_reply_to_id')
@ -100,7 +120,8 @@ class SearchController extends Controller
->whereProfileId(Auth::user()->profile->id)
->where('caption', 'like', '%'.$tag.'%')
->orWhere('uri', $tag)
->orderBy('created_at', 'desc')
->latest()
->limit(10)
->get();
if($posts->count() > 0) {
@ -115,11 +136,9 @@ class SearchController extends Controller
'thumb' => $item->thumb(),
];
});
$tokens = $tokens->push($posts);
}
if($tokens->count() > 0) {
$tokens = $tokens[0];
$tokens['posts'] = $posts;
}
return response()->json($tokens);
}

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.8.4',
'version' => '0.8.5',
/*
|--------------------------------------------------------------------------

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/css/appdark.css vendored

Binary file not shown.

BIN
public/css/landing.css vendored Normal file

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/landing.js vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/search.js vendored Normal file

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -3,8 +3,6 @@ window.Popper = require('popper.js').default;
window.pixelfed = window.pixelfed || {};
window.$ = window.jQuery = require('jquery');
require('bootstrap');
window.typeahead = require('./lib/typeahead');
window.Bloodhound = require('./lib/bloodhound');
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
require('readmore-js');

View file

@ -38,7 +38,7 @@ import swal from 'sweetalert';
// require('./components/localstorage');
// require('./components/commentform');
require('./components/searchform');
//require('./components/searchform');
// require('./components/bookmarkform');
// require('./components/statusform');
//require('./components/embed');
@ -63,6 +63,11 @@ require('./components/searchform');
// Initialize Notification Helper
window.pixelfed.n = {};
// Vue.component(
// 'search-results',
// require('./components/SearchResults.vue').default
// );
// Vue.component(
// 'photo-presenter',
// require('./components/presenter/PhotoPresenter.vue').default

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,3 @@
<style scoped>
.status-comments,
.reactions,
.col-md-4 {
background: #fff;
}
.postPresenterContainer {
background: #fff;
}
@media(min-width: 720px) {
.postPresenterContainer {
min-height: 600px;
}
}
</style>
<template>
<div class="postComponent d-none">
<div class="container px-0">
@ -40,7 +25,7 @@
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
</div>
<div v-if="ownerOrAdmin()">
<!-- <a class="dropdown-item font-weight-bold" :href="editUrl()">Disable Comments</a> -->
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</div>
@ -92,19 +77,18 @@
</a>
<div class="float-right">
<div class="post-actions">
<div class="dropdown">
<div v-if="user != false" class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<span class="menu-user d-none">
<span v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
</span>
<span class="menu-author d-none">
<!-- <a class="dropdown-item font-weight-bold" :href="editUrl()">Mute Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Disable Comments</a> -->
<span v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
</span>
@ -114,19 +98,49 @@
</div>
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100">
<div class="card-body status-comments">
<div class="card-body status-comments pb-5">
<div class="status-comment">
<p class="mb-1 read-more" style="overflow: hidden;">
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
</p>
<post-comments :user="this.user" :post-id="statusId" :post-username="statusUsername"></post-comments>
<div v-if="showComments">
<div class="postCommentsLoader text-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="postCommentsContainer d-none pt-3">
<p class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
<div class="comments" data-min-id="0" data-max-id="0">
<div v-for="(reply, index) in results" class="pb-3">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
<a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
<span class="text-break" v-html="reply.content"></span>
</span>
<span class="pl-2" style="min-width:38px">
<span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
</span>
</p>
<p class="">
<span class="text-muted mr-3" style="width: 20px;" v-text="timeAgo(reply.created_at)"></span>
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply)">Reply</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body flex-grow-0 py-1">
<div class="reactions my-1">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus"></h3>
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
</div>
@ -145,15 +159,29 @@
</div>
</div>
</div>
<div class="card-footer bg-white sticky-md-bottom">
<div class="comment-form-guest">
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction">😂</li>
<li class="nav-item" v-on:click="emojiReaction">💯</li>
<li class="nav-item" v-on:click="emojiReaction"></li>
<li class="nav-item" v-on:click="emojiReaction">🙌</li>
<li class="nav-item" v-on:click="emojiReaction">👏</li>
<li class="nav-item" v-on:click="emojiReaction">😍</li>
<li class="nav-item" v-on:click="emojiReaction">😯</li>
<li class="nav-item" v-on:click="emojiReaction">😢</li>
<li class="nav-item" v-on:click="emojiReaction">😅</li>
<li class="nav-item" v-on:click="emojiReaction">😁</li>
<li class="nav-item" v-on:click="emojiReaction">🙂</li>
<li class="nav-item" v-on:click="emojiReaction">😎</li>
</ul>
</div>
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
<div v-if="user.length == 0" class="comment-form-guest p-3">
<a href="/login">Login</a> to like or comment.
</div>
<form class="comment-form d-none" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
<input type="hidden" name="_token" value="">
<input type="hidden" name="item" :value="statusId">
<input class="form-control" name="comment" placeholder="Add a comment…" autocomplete="off">
<input type="submit" value="Send" class="btn btn-primary comment-submit" />
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
</form>
</div>
</div>
@ -243,6 +271,68 @@
</div>
</template>
<style type="text/css" scoped>
.status-comments,
.reactions,
.col-md-4 {
background: #fff;
}
.postPresenterContainer {
background: #fff;
}
@media(min-width: 720px) {
.postPresenterContainer {
min-height: 600px;
}
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
}
.reply-btn {
position: absolute;
bottom: 12px;
right: 20px;
width: 60px;
text-align: center;
border-radius: 0 3px 3px 0;
}
.text-lighter {
color:#B8C2CC !important;
}
.text-break {
overflow-wrap: break-word;
}
.comments p {
margin-bottom: 0;
}
.comment-reaction {
font-size: 80%;
}
.show-reply-bar {
display: inline-block;
border-bottom: 1px solid #999;
height: 0;
margin-right: 16px;
vertical-align: middle;
width: 24px;
}
.comment-thread {
margin: 4px 0 0 40px;
width: calc(100% - 40px);
}
.emoji-reactions .nav-item {
font-size: 1.2rem;
padding: 7px;
cursor: pointer;
}
.emoji-reactions::-webkit-scrollbar {
width: 0px;
height: 0px;
background: transparent;
}
</style>
<script>
pixelfed.postComponent = {};
@ -262,7 +352,16 @@ export default {
likesPage: 1,
shares: [],
sharesPage: 1,
lightboxMedia: false
lightboxMedia: false,
replyText: '',
results: [],
pagination: {},
min_id: 0,
max_id: 0,
reply_to_profile_id: 0,
thread: false,
showComments: false
}
},
@ -279,6 +378,8 @@ export default {
updated() {
$('.carousel').carousel();
pixelfed.readmore();
if(this.reactions) {
if(this.reactions.bookmarked == true) {
$('.postComponent .far.fa-bookmark').removeClass('far').addClass('fas text-warning');
@ -345,6 +446,10 @@ export default {
this.showMuteBlock();
loader.hide();
pixelfed.readmore();
if(self.status.comments_disabled == false) {
self.showComments = true;
this.fetchComments();
}
$('.postComponent').removeClass('d-none');
$('.postPresenterLoader').addClass('d-none');
$('.postPresenterContainer').removeClass('d-none');
@ -369,10 +474,6 @@ export default {
});
},
commentFocus() {
$('.comment-form input[name="comment"]').focus();
},
likesModal() {
if(this.status.favourites_count == 0 || $('body').hasClass('loggedIn') == false) {
return;
@ -422,6 +523,7 @@ export default {
likeStatus(event) {
if($('body').hasClass('loggedIn') == false) {
window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
@ -448,6 +550,7 @@ export default {
shareStatus() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
@ -474,6 +577,7 @@ export default {
bookmarkStatus() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
@ -521,6 +625,9 @@ export default {
},
deletePost(status) {
if(!this.ownerOrAdmin()) {
return;
}
var result = confirm('Are you sure you want to delete this post?');
if (result) {
if($('body').hasClass('loggedIn') == false) {
@ -553,6 +660,198 @@ export default {
lightbox(src) {
this.lightboxMedia = src;
this.$refs.lightboxModal.show();
},
postReply() {
let self = this;
if(this.replyText.length == 0 ||
this.replyText.trim() == '@'+this.status.account.acct) {
self.replyText = null;
$('textarea[name="comment"]').blur();
return;
}
let data = {
item: this.statusId,
comment: this.replyText
}
axios.post('/i/comment', data)
.then(function(res) {
let entity = res.data.entity;
self.results.push(entity);
self.replyText = '';
let elem = $('.status-comments')[0];
elem.scrollTop = elem.clientHeight;
});
},
deleteComment(id, i) {
axios.post('/i/delete', {
type: 'comment',
item: id
}).then(res => {
this.results.splice(i, 1);
}).catch(err => {
swal('Something went wrong!', 'Please try again later', 'error');
});
},
l(e) {
let len = e.length;
if(len < 10) { return e; }
return e.substr(0, 10)+'...';
},
replyFocus(e) {
this.reply_to_profile_id = e.account.id;
this.replyText = '@' + e.account.username + ' ';
$('textarea[name="comment"]').focus();
},
fetchComments() {
let url = '/api/v2/comments/'+this.statusUsername+'/status/'+this.statusId;
axios.get(url)
.then(response => {
let self = this;
this.results = _.reverse(response.data.data);
this.pagination = response.data.meta.pagination;
if(this.results.length > 0) {
$('.load-more-link').removeClass('d-none');
}
$('.postCommentsLoader').addClass('d-none');
$('.postCommentsContainer').removeClass('d-none');
}).catch(error => {
if(!error.response) {
$('.postCommentsLoader .lds-ring')
.attr('style','width:100%')
.addClass('pt-4 font-weight-bold text-muted')
.text('An error occurred, cannot fetch comments. Please try again later.');
} else {
switch(error.response.status) {
case 401:
$('.postCommentsLoader .lds-ring')
.attr('style','width:100%')
.addClass('pt-4 font-weight-bold text-muted')
.text('Please login to view.');
break;
default:
$('.postCommentsLoader .lds-ring')
.attr('style','width:100%')
.addClass('pt-4 font-weight-bold text-muted')
.text('An error occurred, cannot fetch comments. Please try again later.');
break;
}
}
});
},
loadMore(e) {
e.preventDefault();
if(this.pagination.total_pages == 1 || this.pagination.current_page == this.pagination.total_pages) {
$('.load-more-link').addClass('d-none');
return;
}
$('.postCommentsLoader').removeClass('d-none');
let next = this.pagination.links.next;
axios.get(next)
.then(response => {
let self = this;
let res = response.data.data;
$('.postCommentsLoader').addClass('d-none');
for(let i=0; i < res.length; i++) {
this.results.unshift(res[i]);
}
this.pagination = response.data.meta.pagination;
});
},
likeReply(status, $event) {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.post('/i/like', {
item: status.id
}).then(res => {
status.favourites_count = res.data.count;
if(status.favourited == true) {
status.favourited = false;
} else {
status.favourited = true;
}
}).catch(err => {
swal('Error', 'Something went wrong, please try again later.', 'error');
});
},
truncate(str,lim) {
return _.truncate(str,{
length: lim
});
},
timeAgo(ts) {
let date = Date.parse(ts);
let seconds = Math.floor((new Date() - date) / 1000);
let interval = Math.floor(seconds / 31536000);
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";
},
emojiReaction() {
let em = event.target.innerText;
if(this.replyText.length == 0) {
this.reply_to_profile_id = this.status.account.id;
this.replyText = '@' + this.status.account.username + ' ' + em;
$('textarea[name="comment"]').focus();
} else {
this.reply_to_profile_id = this.status.account.id;
this.replyText += em;
$('textarea[name="comment"]').focus();
}
},
toggleCommentVisibility() {
if(this.ownerOrAdmin() == false) {
return;
}
let state = this.status.comments_disabled;
let self = this;
if(state == true) {
// re-enable comments
axios.post('/i/visibility', {
item: self.status.id,
disableComments: false
}).then(function(res) {
window.location.href = self.status.url;
}).catch(function(err) {
return;
});
} else {
// disable comments
axios.post('/i/visibility', {
item: self.status.id,
disableComments: true
}).then(function(res) {
self.status.comments_disabled = false;
self.showComments = false;
}).catch(function(err) {
return;
});
}
}
},

View file

@ -8,33 +8,56 @@
<div class="container">
<div class="row">
<div class="col-12 col-md-4 d-flex">
<div class="profile-avatar mx-auto">
<div class="profile-avatar mx-md-auto">
<div class="d-block d-md-none">
<div class="row">
<div class="col-5">
<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
</div>
<div class="col-7 pl-2">
<p class="font-weight-ultralight h3 mb-0">{{profile.username}}</p>
<p v-if="profile.id == user.id && user.hasOwnProperty('id')">
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
</p>
<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
<p class="mt-3 mb-0" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button>
</p>
<p class="mt-3 mb-0" v-if="!relationship.following">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button>
</p>
</div>
</div>
</div>
</div>
<div class="d-none d-md-block">
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
</div>
</div>
</div>
<div class="col-12 col-md-8 d-flex align-items-center">
<div class="profile-details">
<div class="username-bar pb-2 d-flex align-items-center">
<span class="font-weight-ultralight h1">{{profile.username}}</span>
<div class="d-none d-md-flex username-bar pb-2 align-items-center">
<span class="font-weight-ultralight h3">{{profile.username}}</span>
<span class="pl-4" v-if="profile.is_admin">
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
</span>
<span class="pl-4">
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted"></a>
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
</span>
<span class="pl-4" v-if="owner">
<a class="fas fa-cog fa-lg text-muted" href="/settings/home"></a>
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
</span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<span class="pl-4" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-secondary font-weight-bold px-4 py-0" v-on:click="followProfile()">Unfollow</button>
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button>
</span>
<span class="pl-4" v-if="!relationship.following">
<button type="button" class="btn btn-primary font-weight-bold px-4 py-0" v-on:click="followProfile()">Follow</button>
<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button>
</span>
</span>
</div>
<div class="profile-stats pb-3 d-inline-flex lead">
<div class="d-none d-md-inline-flex profile-stats pb-3 lead">
<div class="font-weight-light pr-5">
<a class="text-dark" :href="profile.url">
<span class="font-weight-bold">{{profile.statuses_count}}</span>
@ -54,7 +77,7 @@
</a>
</div>
</div>
<p class="lead mb-0 d-flex align-items-center">
<p class="lead mb-0 d-flex align-items-center pt-3">
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
</p>
<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
@ -64,22 +87,50 @@
</div>
</div>
</div>
<div>
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
<ul class="nav d-flex justify-content-center">
<li class="nav-item">
<div class="font-weight-light">
<span class="text-dark text-center">
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<p class="text-muted mb-0">Posts</p>
</span>
</div>
</li>
<li class="nav-item px-5">
<div v-if="profileSettings.followers.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
<p class="text-muted mb-0">Followers</p>
</a>
</div>
</li>
<li class="nav-item">
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
<p class="text-muted mb-0">Following</p>
</a>
</div>
</li>
</ul>
</div>
<div class="bg-white">
<ul class="nav nav-topbar d-flex justify-content-center border-0">
<!-- <li class="nav-item">
<a class="nav-link active font-weight-bold text-uppercase" :href="profile.url">Posts</a>
</li>
-->
<li class="nav-item">
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th"></i></a>
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
</li>
<!-- <li class="nav-item">
<a :class="this.mode == 'masonry' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('masonry')"><i class="fas fa-th-large"></i></a>
</li> -->
<li class="nav-item">
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list"></i></a>
<li class="nav-item px-3">
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
</li>
<li class="nav-item" v-if="owner">
@ -89,7 +140,7 @@
</div>
<div class="container">
<div class="profile-timeline mt-2 mt-md-4">
<div class="profile-timeline mt-md-4">
<div class="row" v-if="mode == 'grid'">
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url">
@ -116,8 +167,8 @@
</div>
</div>
<div class="row" v-if="mode == 'list'">
<div class="col-md-8 col-lg-8 offset-md-2 pt-2 px-0 my-3 timeline">
<div class="card mb-4 status-card card-md-rounded-0" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
<div class="card status-card card-md-rounded-0" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
<div class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">

View file

@ -11,7 +11,7 @@
<div v-if="!loading && !networkError" class="mt-5 row">
<div class="col-12 col-md-3">
<div class="col-12 col-md-3 mb-4">
<div>
<p class="font-weight-bold">Filters</p>
<div class="custom-control custom-checkbox">
@ -29,14 +29,14 @@
</div>
</div>
<div class="col-12 col-md-9">
<p class="h3 font-weight-lighter">Showing results for <i>{{query}}</i></p>
<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
<hr>
<div v-if="filters.hashtags && results.hashtags.length" class="row mb-4">
<div v-if="filters.hashtags && results.hashtags" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Hashtags</p>
<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-4" style="text-decoration: none;" :href="hashtag.url">
<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url">
<div class="card card-body text-center">
<p class="lead mb-0 text-truncate text-dark">
<p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value">
#{{hashtag.value}}
</p>
<p class="lead mb-0 small font-weight-bold text-dark">
@ -46,28 +46,42 @@
</a>
</div>
<div v-if="filters.profiles && results.profiles.length" class="row mb-4">
<div v-if="filters.profiles && results.profiles" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Profiles</p>
<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4" style="text-decoration: none;" :href="profile.url">
<div class="card card-body text-center border-left-primary">
<p class="lead mb-0 text-truncate text-dark">
<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url">
<div class="card card-body text-center">
<p class="text-center">
<img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow">
</p>
<p class="font-weight-bold text-truncate text-dark">
{{profile.value}}
</p>
<!-- <p class="mb-0 text-center">
<button :class="[profile.entity.following ? 'btn btn-secondary btn-sm py-1 font-weight-bold' : 'btn btn-primary btn-sm py-1 font-weight-bold']" v-on:click="followProfile(profile.entity.id)">
{{profile.entity.following ? 'Unfollow' : 'Follow'}}
</button>
</p> -->
</div>
</a>
</div>
<div v-if="filters.statuses && results.statuses.length" class="row mb-4">
<div v-if="filters.statuses && results.statuses" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Statuses</p>
<a v-for="(status, index) in results.statuses" class="col-12 col-md-4" style="text-decoration: none;" :href="status.url">
<div class="card card-body text-center border-left-primary">
<p class="lead mb-0 text-truncate text-dark">
{{status.value}}
<a v-for="(status, index) in results.statuses" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="status.url">
<div class="card">
<img class="card-img-top img-fluid" :src="status.thumb" style="height:180px;">
<div class="card-body text-center ">
<p class="mb-0 small text-truncate font-weight-bold text-muted" v-html="status.value">
</p>
</div>
</div>
</a>
</div>
<div v-if="!results.hashtags && !results.profiles && !results.statuses">
<p class="text-center lead">No results found!</p>
</div>
</div>
</div>
@ -81,7 +95,7 @@
<script type="text/javascript">
export default {
props: ['query'],
props: ['query', 'profileId'],
data() {
return {
@ -103,31 +117,35 @@ export default {
this.fetchSearchResults();
},
mounted() {
$('.search-form input').val(this.query);
$('.search-bar input').val(this.query);
},
updated() {
},
methods: {
fetchSearchResults() {
axios.get('/api/search/' + this.query)
axios.get('/api/search/' + encodeURI(this.query))
.then(res => {
let results = res.data;
this.results.hashtags = results.filter(i => {
return i.type == 'hashtag';
});
this.results.profiles = results.filter(i => {
return i.type == 'profile';
});
this.results.statuses = results.filter(i => {
return i.type == 'status';
});
this.results.hashtags = results.hashtags;
this.results.profiles = results.profiles;
this.results.statuses = results.posts;
this.loading = false;
}).catch(err => {
this.loading = false;
this.networkError = true;
// this.networkError = true;
})
},
followProfile(id) {
// todo: finish AP Accept handling to enable remote follows
return;
// axios.post('/i/follow', {
// item: id
// }).then(res => {
// window.location.href = window.location.href;
// });
},
}
}

View file

@ -1,71 +0,0 @@
$(document).ready(function() {
let queryEngine = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: process.env.MIX_API_SEARCH + '/%QUERY%',
wildcard: '%QUERY%'
}
});
$('.search-form .search-form-input').typeahead(null, {
name: 'search',
display: 'value',
source: queryEngine,
limit: 40,
templates: {
empty: [
'<div class="alert alert-info mb-0 font-weight-bold">',
'No Results Found',
'</div>'
].join('\n'),
suggestion: function(data) {
let type = data.type;
let res = false;
switch(type) {
case 'hashtag':
res = '<a href="'+data.url+'?src=search">' +
'<div class="media d-flex align-items-center">' +
'<div class="mr-3 h4 text-muted"><span class="fas fa-hashtag"></span></div>' +
'<div class="media-body text-truncate">' +
'<p class="mt-0 mb-0 font-weight-bold">'+data.value+'</p>' +
'<p class="text-muted mb-0">'+data.count+' posts</p>' +
'</div>' +
'</div>' +
'</a>';
break;
case 'profile':
res = '<a href="'+data.url+'?src=search">' +
'<div class="media d-flex align-items-center">' +
'<div class="mr-3 h4 text-muted"><span class="far fa-user"></span></div>' +
'<div class="media-body text-truncate">' +
'<p class="mt-0 mb-0 font-weight-bold">'+data.name+'</p>' +
'<p class="text-muted mb-0">'+data.value+'</p>' +
'</div>' +
'</div>' +
'</a>';
break;
case 'status':
res = '<a href="'+data.url+'?src=search">' +
'<div class="media d-flex align-items-center">' +
'<div class="mr-3 h4 text-muted"><img src="'+data.thumb+'" width="32px"></div>' +
'<div class="media-body text-truncate">' +
'<p class="mt-0 mb-0 font-weight-bold">'+data.name+'</p>' +
'<p class="text-muted mb-0 small">'+data.value+'</p>' +
'</div>' +
'</div>' +
'</a>';
break;
default:
res = false;
break;
}
if(res !== false) {
return res;
}
}
}
});
});

View file

@ -1,10 +0,0 @@
window.Vue = require('vue');
Vue.component(
'landing-page',
require('./components/LandingPage.vue').default
);
new Vue({
el: '#content'
});

View file

@ -1,952 +0,0 @@
/*!
* typeahead.js 1.2.0
* https://github.com/twitter/typeahead.js
* Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
*/
(function(root, factory) {
if (typeof define === "function" && define.amd) {
define([ "jquery" ], function(a0) {
return root["Bloodhound"] = factory(a0);
});
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"));
} else {
root["Bloodhound"] = factory(root["jQuery"]);
}
})(this, function($) {
var _ = function() {
"use strict";
return {
isMsie: function() {
return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
},
isBlankString: function(str) {
return !str || /^\s*$/.test(str);
},
escapeRegExChars: function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
isString: function(obj) {
return typeof obj === "string";
},
isNumber: function(obj) {
return typeof obj === "number";
},
isArray: $.isArray,
isFunction: $.isFunction,
isObject: $.isPlainObject,
isUndefined: function(obj) {
return typeof obj === "undefined";
},
isElement: function(obj) {
return !!(obj && obj.nodeType === 1);
},
isJQuery: function(obj) {
return obj instanceof $;
},
toStr: function toStr(s) {
return _.isUndefined(s) || s === null ? "" : s + "";
},
bind: $.proxy,
each: function(collection, cb) {
$.each(collection, reverseArgs);
function reverseArgs(index, value) {
return cb(value, index);
}
},
map: $.map,
filter: $.grep,
every: function(obj, test) {
var result = true;
if (!obj) {
return result;
}
$.each(obj, function(key, val) {
if (!(result = test.call(null, val, key, obj))) {
return false;
}
});
return !!result;
},
some: function(obj, test) {
var result = false;
if (!obj) {
return result;
}
$.each(obj, function(key, val) {
if (result = test.call(null, val, key, obj)) {
return false;
}
});
return !!result;
},
mixin: $.extend,
identity: function(x) {
return x;
},
clone: function(obj) {
return $.extend(true, {}, obj);
},
getIdGenerator: function() {
var counter = 0;
return function() {
return counter++;
};
},
templatify: function templatify(obj) {
return $.isFunction(obj) ? obj : template;
function template() {
return String(obj);
}
},
defer: function(fn) {
setTimeout(fn, 0);
},
debounce: function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments, later, callNow;
later = function() {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
}
};
callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
}
return result;
};
},
throttle: function(func, wait) {
var context, args, timeout, result, previous, later;
previous = 0;
later = function() {
previous = new Date();
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date(), remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val);
},
guid: function() {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
},
noop: function() {}
};
}();
var VERSION = "1.2.0";
var tokenizers = function() {
"use strict";
return {
nonword: nonword,
whitespace: whitespace,
ngram: ngram,
obj: {
nonword: getObjTokenizer(nonword),
whitespace: getObjTokenizer(whitespace),
ngram: getObjTokenizer(ngram)
}
};
function whitespace(str) {
str = _.toStr(str);
return str ? str.split(/\s+/) : [];
}
function nonword(str) {
str = _.toStr(str);
return str ? str.split(/\W+/) : [];
}
function ngram(str) {
str = _.toStr(str);
var tokens = [], word = "";
_.each(str.split(""), function(char) {
if (char.match(/\s+/)) {
word = "";
} else {
tokens.push(word + char);
word += char;
}
});
return tokens;
}
function getObjTokenizer(tokenizer) {
return function setKey(keys) {
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
return function tokenize(o) {
var tokens = [];
_.each(keys, function(k) {
tokens = tokens.concat(tokenizer(_.toStr(o[k])));
});
return tokens;
};
};
}
}();
var LruCache = function() {
"use strict";
function LruCache(maxSize) {
this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
this.reset();
if (this.maxSize <= 0) {
this.set = this.get = $.noop;
}
}
_.mixin(LruCache.prototype, {
set: function set(key, val) {
var tailItem = this.list.tail, node;
if (this.size >= this.maxSize) {
this.list.remove(tailItem);
delete this.hash[tailItem.key];
this.size--;
}
if (node = this.hash[key]) {
node.val = val;
this.list.moveToFront(node);
} else {
node = new Node(key, val);
this.list.add(node);
this.hash[key] = node;
this.size++;
}
},
get: function get(key) {
var node = this.hash[key];
if (node) {
this.list.moveToFront(node);
return node.val;
}
},
reset: function reset() {
this.size = 0;
this.hash = {};
this.list = new List();
}
});
function List() {
this.head = this.tail = null;
}
_.mixin(List.prototype, {
add: function add(node) {
if (this.head) {
node.next = this.head;
this.head.prev = node;
}
this.head = node;
this.tail = this.tail || node;
},
remove: function remove(node) {
node.prev ? node.prev.next = node.next : this.head = node.next;
node.next ? node.next.prev = node.prev : this.tail = node.prev;
},
moveToFront: function(node) {
this.remove(node);
this.add(node);
}
});
function Node(key, val) {
this.key = key;
this.val = val;
this.prev = this.next = null;
}
return LruCache;
}();
var PersistentStorage = function() {
"use strict";
var LOCAL_STORAGE;
try {
LOCAL_STORAGE = window.localStorage;
LOCAL_STORAGE.setItem("~~~", "!");
LOCAL_STORAGE.removeItem("~~~");
} catch (err) {
LOCAL_STORAGE = null;
}
function PersistentStorage(namespace, override) {
this.prefix = [ "__", namespace, "__" ].join("");
this.ttlKey = "__ttl__";
this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
this.ls = override || LOCAL_STORAGE;
!this.ls && this._noop();
}
_.mixin(PersistentStorage.prototype, {
_prefix: function(key) {
return this.prefix + key;
},
_ttlKey: function(key) {
return this._prefix(key) + this.ttlKey;
},
_noop: function() {
this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
},
_safeSet: function(key, val) {
try {
this.ls.setItem(key, val);
} catch (err) {
if (err.name === "QuotaExceededError") {
this.clear();
this._noop();
}
}
},
get: function(key) {
if (this.isExpired(key)) {
this.remove(key);
}
return decode(this.ls.getItem(this._prefix(key)));
},
set: function(key, val, ttl) {
if (_.isNumber(ttl)) {
this._safeSet(this._ttlKey(key), encode(now() + ttl));
} else {
this.ls.removeItem(this._ttlKey(key));
}
return this._safeSet(this._prefix(key), encode(val));
},
remove: function(key) {
this.ls.removeItem(this._ttlKey(key));
this.ls.removeItem(this._prefix(key));
return this;
},
clear: function() {
var i, keys = gatherMatchingKeys(this.keyMatcher);
for (i = keys.length; i--; ) {
this.remove(keys[i]);
}
return this;
},
isExpired: function(key) {
var ttl = decode(this.ls.getItem(this._ttlKey(key)));
return _.isNumber(ttl) && now() > ttl ? true : false;
}
});
return PersistentStorage;
function now() {
return new Date().getTime();
}
function encode(val) {
return JSON.stringify(_.isUndefined(val) ? null : val);
}
function decode(val) {
return $.parseJSON(val);
}
function gatherMatchingKeys(keyMatcher) {
var i, key, keys = [], len = LOCAL_STORAGE.length;
for (i = 0; i < len; i++) {
if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
keys.push(key.replace(keyMatcher, ""));
}
}
return keys;
}
}();
var Transport = function() {
"use strict";
var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
function Transport(o) {
o = o || {};
this.maxPendingRequests = o.maxPendingRequests || 6;
this.cancelled = false;
this.lastReq = null;
this._send = o.transport;
this._get = o.limiter ? o.limiter(this._get) : this._get;
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
}
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
this.maxPendingRequests = num;
};
Transport.resetCache = function resetCache() {
sharedCache.reset();
};
_.mixin(Transport.prototype, {
_fingerprint: function fingerprint(o) {
o = o || {};
return o.url + o.type + $.param(o.data || {});
},
_get: function(o, cb) {
var that = this, fingerprint, jqXhr;
fingerprint = this._fingerprint(o);
if (this.cancelled || fingerprint !== this.lastReq) {
return;
}
if (jqXhr = pendingRequests[fingerprint]) {
jqXhr.done(done).fail(fail);
} else if (pendingRequestsCount < this.maxPendingRequests) {
pendingRequestsCount++;
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
} else {
this.onDeckRequestArgs = [].slice.call(arguments, 0);
}
function done(resp) {
cb(null, resp);
that._cache.set(fingerprint, resp);
}
function fail() {
cb(true);
}
function always() {
pendingRequestsCount--;
delete pendingRequests[fingerprint];
if (that.onDeckRequestArgs) {
that._get.apply(that, that.onDeckRequestArgs);
that.onDeckRequestArgs = null;
}
}
},
get: function(o, cb) {
var resp, fingerprint;
cb = cb || $.noop;
o = _.isString(o) ? {
url: o
} : o || {};
fingerprint = this._fingerprint(o);
this.cancelled = false;
this.lastReq = fingerprint;
if (resp = this._cache.get(fingerprint)) {
cb(null, resp);
} else {
this._get(o, cb);
}
},
cancel: function() {
this.cancelled = true;
}
});
return Transport;
}();
var SearchIndex = window.SearchIndex = function() {
"use strict";
var CHILDREN = "c", IDS = "i";
function SearchIndex(o) {
o = o || {};
if (!o.datumTokenizer || !o.queryTokenizer) {
$.error("datumTokenizer and queryTokenizer are both required");
}
this.identify = o.identify || _.stringify;
this.datumTokenizer = o.datumTokenizer;
this.queryTokenizer = o.queryTokenizer;
this.matchAnyQueryToken = o.matchAnyQueryToken;
this.reset();
}
_.mixin(SearchIndex.prototype, {
bootstrap: function bootstrap(o) {
this.datums = o.datums;
this.trie = o.trie;
},
add: function(data) {
var that = this;
data = _.isArray(data) ? data : [ data ];
_.each(data, function(datum) {
var id, tokens;
that.datums[id = that.identify(datum)] = datum;
tokens = normalizeTokens(that.datumTokenizer(datum));
_.each(tokens, function(token) {
var node, chars, ch;
node = that.trie;
chars = token.split("");
while (ch = chars.shift()) {
node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
node[IDS].push(id);
}
});
});
},
get: function get(ids) {
var that = this;
return _.map(ids, function(id) {
return that.datums[id];
});
},
search: function search(query) {
var that = this, tokens, matches;
tokens = normalizeTokens(this.queryTokenizer(query));
_.each(tokens, function(token) {
var node, chars, ch, ids;
if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
return false;
}
node = that.trie;
chars = token.split("");
while (node && (ch = chars.shift())) {
node = node[CHILDREN][ch];
}
if (node && chars.length === 0) {
ids = node[IDS].slice(0);
matches = matches ? getIntersection(matches, ids) : ids;
} else {
if (!that.matchAnyQueryToken) {
matches = [];
return false;
}
}
});
return matches ? _.map(unique(matches), function(id) {
return that.datums[id];
}) : [];
},
all: function all() {
var values = [];
for (var key in this.datums) {
values.push(this.datums[key]);
}
return values;
},
reset: function reset() {
this.datums = {};
this.trie = newNode();
},
serialize: function serialize() {
return {
datums: this.datums,
trie: this.trie
};
}
});
return SearchIndex;
function normalizeTokens(tokens) {
tokens = _.filter(tokens, function(token) {
return !!token;
});
tokens = _.map(tokens, function(token) {
return token.toLowerCase();
});
return tokens;
}
function newNode() {
var node = {};
node[IDS] = [];
node[CHILDREN] = {};
return node;
}
function unique(array) {
var seen = {}, uniques = [];
for (var i = 0, len = array.length; i < len; i++) {
if (!seen[array[i]]) {
seen[array[i]] = true;
uniques.push(array[i]);
}
}
return uniques;
}
function getIntersection(arrayA, arrayB) {
var ai = 0, bi = 0, intersection = [];
arrayA = arrayA.sort();
arrayB = arrayB.sort();
var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
while (ai < lenArrayA && bi < lenArrayB) {
if (arrayA[ai] < arrayB[bi]) {
ai++;
} else if (arrayA[ai] > arrayB[bi]) {
bi++;
} else {
intersection.push(arrayA[ai]);
ai++;
bi++;
}
}
return intersection;
}
}();
var Prefetch = function() {
"use strict";
var keys;
keys = {
data: "data",
protocol: "protocol",
thumbprint: "thumbprint"
};
function Prefetch(o) {
this.url = o.url;
this.ttl = o.ttl;
this.cache = o.cache;
this.prepare = o.prepare;
this.transform = o.transform;
this.transport = o.transport;
this.thumbprint = o.thumbprint;
this.storage = new PersistentStorage(o.cacheKey);
}
_.mixin(Prefetch.prototype, {
_settings: function settings() {
return {
url: this.url,
type: "GET",
dataType: "json"
};
},
store: function store(data) {
if (!this.cache) {
return;
}
this.storage.set(keys.data, data, this.ttl);
this.storage.set(keys.protocol, location.protocol, this.ttl);
this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
},
fromCache: function fromCache() {
var stored = {}, isExpired;
if (!this.cache) {
return null;
}
stored.data = this.storage.get(keys.data);
stored.protocol = this.storage.get(keys.protocol);
stored.thumbprint = this.storage.get(keys.thumbprint);
isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
return stored.data && !isExpired ? stored.data : null;
},
fromNetwork: function(cb) {
var that = this, settings;
if (!cb) {
return;
}
settings = this.prepare(this._settings());
this.transport(settings).fail(onError).done(onResponse);
function onError() {
cb(true);
}
function onResponse(resp) {
cb(null, that.transform(resp));
}
},
clear: function clear() {
this.storage.clear();
return this;
}
});
return Prefetch;
}();
var Remote = function() {
"use strict";
function Remote(o) {
this.url = o.url;
this.prepare = o.prepare;
this.transform = o.transform;
this.indexResponse = o.indexResponse;
this.transport = new Transport({
cache: o.cache,
limiter: o.limiter,
transport: o.transport,
maxPendingRequests: o.maxPendingRequests
});
}
_.mixin(Remote.prototype, {
_settings: function settings() {
return {
url: this.url,
type: "GET",
dataType: "json"
};
},
get: function get(query, cb) {
var that = this, settings;
if (!cb) {
return;
}
query = query || "";
settings = this.prepare(query, this._settings());
return this.transport.get(settings, onResponse);
function onResponse(err, resp) {
err ? cb([]) : cb(that.transform(resp));
}
},
cancelLastRequest: function cancelLastRequest() {
this.transport.cancel();
}
});
return Remote;
}();
var oParser = function() {
"use strict";
return function parse(o) {
var defaults, sorter;
defaults = {
initialize: true,
identify: _.stringify,
datumTokenizer: null,
queryTokenizer: null,
matchAnyQueryToken: false,
sufficient: 5,
indexRemote: false,
sorter: null,
local: [],
prefetch: null,
remote: null
};
o = _.mixin(defaults, o || {});
!o.datumTokenizer && $.error("datumTokenizer is required");
!o.queryTokenizer && $.error("queryTokenizer is required");
sorter = o.sorter;
o.sorter = sorter ? function(x) {
return x.sort(sorter);
} : _.identity;
o.local = _.isFunction(o.local) ? o.local() : o.local;
o.prefetch = parsePrefetch(o.prefetch);
o.remote = parseRemote(o.remote);
return o;
};
function parsePrefetch(o) {
var defaults;
if (!o) {
return null;
}
defaults = {
url: null,
ttl: 24 * 60 * 60 * 1e3,
cache: true,
cacheKey: null,
thumbprint: "",
prepare: _.identity,
transform: _.identity,
transport: null
};
o = _.isString(o) ? {
url: o
} : o;
o = _.mixin(defaults, o);
!o.url && $.error("prefetch requires url to be set");
o.transform = o.filter || o.transform;
o.cacheKey = o.cacheKey || o.url;
o.thumbprint = VERSION + o.thumbprint;
o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
return o;
}
function parseRemote(o) {
var defaults;
if (!o) {
return;
}
defaults = {
url: null,
cache: true,
prepare: null,
replace: null,
wildcard: null,
limiter: null,
rateLimitBy: "debounce",
rateLimitWait: 300,
transform: _.identity,
transport: null
};
o = _.isString(o) ? {
url: o
} : o;
o = _.mixin(defaults, o);
!o.url && $.error("remote requires url to be set");
o.transform = o.filter || o.transform;
o.prepare = toRemotePrepare(o);
o.limiter = toLimiter(o);
o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
delete o.replace;
delete o.wildcard;
delete o.rateLimitBy;
delete o.rateLimitWait;
return o;
}
function toRemotePrepare(o) {
var prepare, replace, wildcard;
prepare = o.prepare;
replace = o.replace;
wildcard = o.wildcard;
if (prepare) {
return prepare;
}
if (replace) {
prepare = prepareByReplace;
} else if (o.wildcard) {
prepare = prepareByWildcard;
} else {
prepare = identityPrepare;
}
return prepare;
function prepareByReplace(query, settings) {
settings.url = replace(settings.url, query);
return settings;
}
function prepareByWildcard(query, settings) {
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
return settings;
}
function identityPrepare(query, settings) {
return settings;
}
}
function toLimiter(o) {
var limiter, method, wait;
limiter = o.limiter;
method = o.rateLimitBy;
wait = o.rateLimitWait;
if (!limiter) {
limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
}
return limiter;
function debounce(wait) {
return function debounce(fn) {
return _.debounce(fn, wait);
};
}
function throttle(wait) {
return function throttle(fn) {
return _.throttle(fn, wait);
};
}
}
function callbackToDeferred(fn) {
return function wrapper(o) {
var deferred = $.Deferred();
fn(o, onSuccess, onError);
return deferred;
function onSuccess(resp) {
_.defer(function() {
deferred.resolve(resp);
});
}
function onError(err) {
_.defer(function() {
deferred.reject(err);
});
}
};
}
}();
var Bloodhound = function() {
"use strict";
var old;
old = window && window.Bloodhound;
function Bloodhound(o) {
o = oParser(o);
this.sorter = o.sorter;
this.identify = o.identify;
this.sufficient = o.sufficient;
this.indexRemote = o.indexRemote;
this.local = o.local;
this.remote = o.remote ? new Remote(o.remote) : null;
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
this.index = new SearchIndex({
identify: this.identify,
datumTokenizer: o.datumTokenizer,
queryTokenizer: o.queryTokenizer
});
o.initialize !== false && this.initialize();
}
Bloodhound.noConflict = function noConflict() {
window && (window.Bloodhound = old);
return Bloodhound;
};
Bloodhound.tokenizers = tokenizers;
_.mixin(Bloodhound.prototype, {
__ttAdapter: function ttAdapter() {
var that = this;
return this.remote ? withAsync : withoutAsync;
function withAsync(query, sync, async) {
return that.search(query, sync, async);
}
function withoutAsync(query, sync) {
return that.search(query, sync);
}
},
_loadPrefetch: function loadPrefetch() {
var that = this, deferred, serialized;
deferred = $.Deferred();
if (!this.prefetch) {
deferred.resolve();
} else if (serialized = this.prefetch.fromCache()) {
this.index.bootstrap(serialized);
deferred.resolve();
} else {
this.prefetch.fromNetwork(done);
}
return deferred.promise();
function done(err, data) {
if (err) {
return deferred.reject();
}
that.add(data);
that.prefetch.store(that.index.serialize());
deferred.resolve();
}
},
_initialize: function initialize() {
var that = this, deferred;
this.clear();
(this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
return this.initPromise;
function addLocalToIndex() {
that.add(that.local);
}
},
initialize: function initialize(force) {
return !this.initPromise || force ? this._initialize() : this.initPromise;
},
add: function add(data) {
this.index.add(data);
return this;
},
get: function get(ids) {
ids = _.isArray(ids) ? ids : [].slice.call(arguments);
return this.index.get(ids);
},
search: function search(query, sync, async) {
var that = this, local;
sync = sync || _.noop;
async = async || _.noop;
local = this.sorter(this.index.search(query));
sync(this.remote ? local.slice() : local);
if (this.remote && local.length < this.sufficient) {
this.remote.get(query, processRemote);
} else if (this.remote) {
this.remote.cancelLastRequest();
}
return this;
function processRemote(remote) {
var nonDuplicates = [];
_.each(remote, function(r) {
!_.some(local, function(l) {
return that.identify(r) === that.identify(l);
}) && nonDuplicates.push(r);
});
that.indexRemote && that.add(nonDuplicates);
async(nonDuplicates);
}
},
all: function all() {
return this.index.all();
},
clear: function clear() {
this.index.reset();
return this;
},
clearPrefetchCache: function clearPrefetchCache() {
this.prefetch && this.prefetch.clear();
return this;
},
clearRemoteCache: function clearRemoteCache() {
Transport.resetCache();
return this;
},
ttAdapter: function ttAdapter() {
return this.__ttAdapter();
}
});
return Bloodhound;
}();
return Bloodhound;
});

File diff suppressed because it is too large Load diff

4
resources/assets/js/search.js vendored Normal file
View file

@ -0,0 +1,4 @@
Vue.component(
'search-results',
require('./components/SearchResults.vue').default
);

View file

@ -37,16 +37,6 @@ body, button, input, textarea {
color: #212529 !important;
}
.search-form {
width: 100%;
}
.search-form input,
.search-form .form-inline,
.search-form .form-control {
width: 100%;
}
.settings-nav .active {
border-left: 2px solid #6c757d !important
}

13
resources/assets/sass/landing.scss vendored Normal file
View file

@ -0,0 +1,13 @@
// Landing Page bundle
@import 'variables';
@import '~bootstrap/scss/bootstrap';
@import 'custom';
@import 'landing/carousel';
@import 'landing/devices';
.container.slim {
width: auto;
max-width: 680px;
padding: 0 15px;
}

View file

@ -0,0 +1,126 @@
@-webkit-keyframes iosDeviceCarousel {
0% {
opacity:1;
}
17% {
opacity:1;
}
25% {
opacity:0;
}
92% {
opacity:0;
}
100% {
opacity:1;
}
}
@-moz-keyframes iosDeviceCarousel {
0% {
opacity:1;
}
17% {
opacity:1;
}
25% {
opacity:0;
}
92% {
opacity:0;
}
100% {
opacity:1;
}
}
@-o-keyframes iosDeviceCarousel {
0% {
opacity:1;
}
17% {
opacity:1;
}
25% {
opacity:0;
}
92% {
opacity:0;
}
100% {
opacity:1;
}
}
@keyframes iosDeviceCarousel {
0% {
opacity:1;
}
17% {
opacity:1;
}
25% {
opacity:0;
}
92% {
opacity:0;
}
100% {
opacity:1;
}
}
#iosDevice {
position:relative;
margin:0 auto;
}
#iosDevice img {
position:absolute;
left:0;
}
#iosDevice img {
-webkit-animation-name: iosDeviceCarousel;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-iteration-count: infinite;
-webkit-animation-duration: 16s;
-moz-animation-name: iosDeviceCarousel;
-moz-animation-timing-function: ease-in-out;
-moz-animation-iteration-count: infinite;
-moz-animation-duration: 16s;
-o-animation-name: iosDeviceCarousel;
-o-animation-timing-function: ease-in-out;
-o-animation-iteration-count: infinite;
-o-animation-duration: 16s;
animation-name: iosDeviceCarousel;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-duration: 16s;
}
#iosDevice img:nth-of-type(1) {
-webkit-animation-delay: 12s;
-moz-animation-delay: 12s;
-o-animation-delay: 12s;
animation-delay: 12s;
}
#iosDevice img:nth-of-type(2) {
-webkit-animation-delay: 8s;
-moz-animation-delay: 8s;
-o-animation-delay: 8s;
animation-delay: 8s;
}
#iosDevice img:nth-of-type(3) {
-webkit-animation-delay: 4s;
-moz-animation-delay: 4s;
-o-animation-delay: 4s;
animation-delay: 4s;
}
#iosDevice img:nth-of-type(4) {
-webkit-animation-delay: 0;
-moz-animation-delay: 0;
-o-animation-delay: 0;
animation-delay: 0;
}

View file

@ -0,0 +1,593 @@
.marvel-device {
display: inline-block;
position: relative;
-webkit-box-sizing: content-box !important;
box-sizing: content-box !important
}
.marvel-device .screen {
width: 100%;
position: relative;
height: 100%;
z-index: 3;
background: white;
overflow: hidden;
display: block;
border-radius: 1px;
-webkit-box-shadow: 0 0 0 3px #111;
box-shadow: 0 0 0 3px #111
}
.marvel-device .top-bar,
.marvel-device .bottom-bar {
height: 3px;
background: black;
width: 100%;
display: block
}
.marvel-device .middle-bar {
width: 3px;
height: 4px;
top: 0px;
left: 90px;
background: black;
position: absolute
}
.marvel-device.iphone-x {
width: 375px;
height: 812px;
padding: 26px;
background: #fdfdfd;
-webkit-box-shadow: inset 0 0 11px 0 black;
box-shadow: inset 0 0 11px 0 black;
border-radius: 66px
}
.marvel-device.iphone-x .overflow {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 66px;
overflow: hidden
}
.marvel-device.iphone-x .shadow {
border-radius: 100%;
width: 90px;
height: 90px;
position: absolute;
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.6) 0%, rgba(255, 255, 255, 0) 60%)
}
.marvel-device.iphone-x .shadow--tl {
top: -20px;
left: -20px
}
.marvel-device.iphone-x .shadow--tr {
top: -20px;
right: -20px
}
.marvel-device.iphone-x .shadow--bl {
bottom: -20px;
left: -20px
}
.marvel-device.iphone-x .shadow--br {
bottom: -20px;
right: -20px
}
.marvel-device.iphone-x:before {
width: calc(100% - 10px);
height: calc(100% - 10px);
position: absolute;
top: 5px;
content: '';
left: 5px;
border-radius: 61px;
background: black;
z-index: 1
}
.marvel-device.iphone-x .inner-shadow {
width: calc(100% - 20px);
height: calc(100% - 20px);
position: absolute;
top: 10px;
overflow: hidden;
left: 10px;
border-radius: 56px;
-webkit-box-shadow: inset 0 0 15px 0 rgba(255, 255, 255, 0.66);
box-shadow: inset 0 0 15px 0 rgba(255, 255, 255, 0.66);
z-index: 1
}
.marvel-device.iphone-x .inner-shadow:before {
-webkit-box-shadow: inset 0 0 20px 0 #FFFFFF;
box-shadow: inset 0 0 20px 0 #FFFFFF;
width: 100%;
height: 116%;
position: absolute;
top: -8%;
content: '';
left: 0;
border-radius: 200px / 112px;
z-index: 2
}
.marvel-device.iphone-x .screen {
border-radius: 40px;
-webkit-box-shadow: none;
box-shadow: none
}
.marvel-device.iphone-x .top-bar,
.marvel-device.iphone-x .bottom-bar {
width: 100%;
position: absolute;
height: 8px;
background: rgba(0, 0, 0, 0.1);
left: 0
}
.marvel-device.iphone-x .top-bar {
top: 80px
}
.marvel-device.iphone-x .bottom-bar {
bottom: 80px
}
.marvel-device.iphone-x .volume,
.marvel-device.iphone-x .volume:before,
.marvel-device.iphone-x .volume:after,
.marvel-device.iphone-x .sleep {
width: 3px;
background: #b5b5b5;
position: absolute
}
.marvel-device.iphone-x .volume {
left: -3px;
top: 116px;
height: 32px
}
.marvel-device.iphone-x .volume:before {
height: 62px;
top: 62px;
content: '';
left: 0
}
.marvel-device.iphone-x .volume:after {
height: 62px;
top: 140px;
content: '';
left: 0
}
.marvel-device.iphone-x .sleep {
height: 96px;
top: 200px;
right: -3px
}
.marvel-device.iphone-x .camera {
width: 6px;
height: 6px;
top: 9px;
border-radius: 100%;
position: absolute;
left: 154px;
background: #0d4d71
}
.marvel-device.iphone-x .speaker {
height: 6px;
width: 60px;
left: 50%;
position: absolute;
top: 9px;
margin-left: -30px;
background: #171818;
border-radius: 6px
}
.marvel-device.iphone-x .notch {
position: absolute;
width: 210px;
height: 30px;
top: 26px;
left: 108px;
z-index: 4;
background: black;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px
}
.marvel-device.iphone-x .notch:before,
.marvel-device.iphone-x .notch:after {
content: '';
height: 8px;
position: absolute;
top: 0;
width: 8px
}
.marvel-device.iphone-x .notch:after {
background: radial-gradient(circle at bottom left, transparent 0, transparent 70%, black 70%, black 100%);
left: -8px
}
.marvel-device.iphone-x .notch:before {
background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
right: -8px
}
.marvel-device.iphone-x.landscape {
height: 375px;
width: 812px
}
.marvel-device.iphone-x.landscape .top-bar,
.marvel-device.iphone-x.landscape .bottom-bar {
width: 8px;
height: 100%;
top: 0
}
.marvel-device.iphone-x.landscape .top-bar {
left: 80px
}
.marvel-device.iphone-x.landscape .bottom-bar {
right: 80px;
bottom: auto;
left: auto
}
.marvel-device.iphone-x.landscape .volume,
.marvel-device.iphone-x.landscape .volume:before,
.marvel-device.iphone-x.landscape .volume:after,
.marvel-device.iphone-x.landscape .sleep {
height: 3px
}
.marvel-device.iphone-x.landscape .inner-shadow:before {
height: 100%;
width: 116%;
left: -8%;
top: 0;
border-radius: 112px / 200px
}
.marvel-device.iphone-x.landscape .volume {
bottom: -3px;
top: auto;
left: 116px;
width: 32px
}
.marvel-device.iphone-x.landscape .volume:before {
width: 62px;
left: 62px;
top: 0
}
.marvel-device.iphone-x.landscape .volume:after {
width: 62px;
left: 140px;
top: 0
}
.marvel-device.iphone-x.landscape .sleep {
width: 96px;
left: 200px;
top: -3px;
right: auto
}
.marvel-device.iphone-x.landscape .camera {
left: 9px;
bottom: 154px;
top: auto
}
.marvel-device.iphone-x.landscape .speaker {
width: 6px;
height: 60px;
left: 9px;
top: 50%;
margin-top: -30px;
margin-left: 0
}
.marvel-device.iphone-x.landscape .notch {
height: 210px;
width: 30px;
left: 26px;
bottom: 108px;
top: auto;
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
border-bottom-left-radius: 0
}
.marvel-device.iphone-x.landscape .notch:before,
.marvel-device.iphone-x.landscape .notch:after {
left: 0
}
.marvel-device.iphone-x.landscape .notch:after {
background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
bottom: -8px;
top: auto
}
.marvel-device.iphone-x.landscape .notch:before {
background: radial-gradient(circle at top right, transparent 0, transparent 70%, black 70%, black 100%);
top: -8px
}
.marvel-device.note8 {
width: 400px;
height: 822px;
background: black;
border-radius: 34px;
padding: 45px 10px
}
.marvel-device.note8 .overflow {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 34px;
overflow: hidden
}
.marvel-device.note8 .speaker {
height: 8px;
width: 56px;
left: 50%;
position: absolute;
top: 25px;
margin-left: -28px;
background: #171818;
z-index: 1;
border-radius: 8px
}
.marvel-device.note8 .camera {
height: 18px;
width: 18px;
left: 86px;
position: absolute;
top: 18px;
background: #212b36;
z-index: 1;
border-radius: 100%
}
.marvel-device.note8 .camera:before {
content: '';
height: 8px;
width: 8px;
left: -22px;
position: absolute;
top: 5px;
background: #212b36;
z-index: 1;
border-radius: 100%
}
.marvel-device.note8 .sensors {
height: 10px;
width: 10px;
left: 120px;
position: absolute;
top: 22px;
background: #1d233b;
z-index: 1;
border-radius: 100%
}
.marvel-device.note8 .sensors:before {
content: '';
height: 10px;
width: 10px;
left: 18px;
position: absolute;
top: 0;
background: #1d233b;
z-index: 1;
border-radius: 100%
}
.marvel-device.note8 .more-sensors {
height: 16px;
width: 16px;
left: 285px;
position: absolute;
top: 18px;
background: #33244a;
-webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
z-index: 1;
border-radius: 100%
}
.marvel-device.note8 .more-sensors:before {
content: '';
height: 11px;
width: 11px;
left: 40px;
position: absolute;
top: 4px;
background: #214a61;
z-index: 1;
border-radius: 100%
}
.marvel-device.note8 .sleep {
width: 2px;
height: 56px;
background: black;
position: absolute;
top: 288px;
right: -2px
}
.marvel-device.note8 .volume {
width: 2px;
height: 120px;
background: black;
position: absolute;
top: 168px;
left: -2px
}
.marvel-device.note8 .volume:before {
content: '';
top: 168px;
width: 2px;
position: absolute;
left: 0;
background: black;
height: 56px
}
.marvel-device.note8 .inner {
width: 100%;
height: calc(100% - 8px);
position: absolute;
top: 2px;
content: '';
left: 0px;
border-radius: 34px;
border-top: 2px solid #9fa0a2;
border-bottom: 2px solid #9fa0a2;
background: black;
z-index: 1;
-webkit-box-shadow: inset 0 0 6px 0 rgba(255, 255, 255, 0.5);
box-shadow: inset 0 0 6px 0 rgba(255, 255, 255, 0.5)
}
.marvel-device.note8 .shadow {
-webkit-box-shadow: inset 0 0 60px 0 white, inset 0 0 30px 0 rgba(255, 255, 255, 0.5), 0 0 20px 0 white, 0 0 20px 0 rgba(255, 255, 255, 0.5);
box-shadow: inset 0 0 60px 0 white, inset 0 0 30px 0 rgba(255, 255, 255, 0.5), 0 0 20px 0 white, 0 0 20px 0 rgba(255, 255, 255, 0.5);
height: 101%;
position: absolute;
top: -0.5%;
content: '';
width: calc(100% - 20px);
left: 10px;
border-radius: 38px;
z-index: 5;
pointer-events: none
}
.marvel-device.note8 .screen {
border-radius: 14px;
-webkit-box-shadow: none;
box-shadow: none
}
.marvel-device.note8.landscape {
height: 400px;
width: 822px;
padding: 10px 45px
}
.marvel-device.note8.landscape .speaker {
height: 56px;
width: 8px;
top: 50%;
margin-top: -28px;
margin-left: 0;
right: 25px;
left: auto
}
.marvel-device.note8.landscape .camera {
top: 86px;
right: 18px;
left: auto
}
.marvel-device.note8.landscape .camera:before {
top: -22px;
left: 5px
}
.marvel-device.note8.landscape .sensors {
top: 120px;
right: 22px;
left: auto
}
.marvel-device.note8.landscape .sensors:before {
top: 18px;
left: 0
}
.marvel-device.note8.landscape .more-sensors {
top: 285px;
right: 18px;
left: auto
}
.marvel-device.note8.landscape .more-sensors:before {
top: 40px;
left: 4px
}
.marvel-device.note8.landscape .sleep {
bottom: -2px;
top: auto;
right: 288px;
width: 56px;
height: 2px
}
.marvel-device.note8.landscape .volume {
width: 120px;
height: 2px;
top: -2px;
right: 168px;
left: auto
}
.marvel-device.note8.landscape .volume:before {
right: 168px;
left: auto;
top: 0;
width: 56px;
height: 2px
}
.marvel-device.note8.landscape .inner {
height: 100%;
width: calc(100% - 8px);
left: 2px;
top: 0;
border-top: 0;
border-bottom: 0;
border-left: 2px solid #9fa0a2;
border-right: 2px solid #9fa0a2
}
.marvel-device.note8.landscape .shadow {
width: 101%;
height: calc(100% - 20px);
left: -0.5%;
top: 10px
}

View file

@ -13,7 +13,7 @@
<div class="col-12 col-md-3">
<div class="card mb-3 border-left-blue">
<div class="card-body text-center">
<p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['pixelfed']}}</p>
<p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['pixelfed']}}" data-toggle="tooltip">{{$sys['pixelfed']}}</p>
</div>
<div class="card-footer font-weight-bold py-0 text-center bg-white">Pixelfed</div>
</div>
@ -21,7 +21,7 @@
<div class="col-12 col-md-3">
<div class="card mb-3 border-left-blue">
<div class="card-body text-center">
<p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['database']['version']}}</p>
<p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['database']['version']}}" data-toggle="tooltip">{{$sys['database']['version']}}</p>
</div>
<div class="card-footer font-weight-bold py-0 text-center bg-white">{{$sys['database']['name']}}</div>
</div>
@ -29,7 +29,7 @@
<div class="col-12 col-md-3">
<div class="card mb-3 border-left-blue">
<div class="card-body text-center">
<p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['php']}}</p>
<p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['php']}}" data-toggle="tooltip">{{$sys['php']}}</p>
</div>
<div class="card-footer font-weight-bold py-0 text-center bg-white">PHP</div>
</div>
@ -37,7 +37,7 @@
<div class="col-12 col-md-3">
<div class="card mb-3 border-left-blue">
<div class="card-body text-center">
<p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['laravel']}}</p>
<p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['laravel']}}" data-toggle="tooltip">{{$sys['laravel']}}</p>
</div>
<div class="card-footer font-weight-bold py-0 text-center bg-white">Laravel</div>
</div>

View file

@ -7,9 +7,14 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@auth
<ul class="navbar-nav ml-auto d-none d-md-block">
<form class="form-inline search-form">
<input class="form-control mr-sm-2 search-form-input" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off">
<ul class="navbar-nav mx-auto pr-3">
<form class="form-inline search-bar" method="get" action="/i/results">
<div class="input-group">
<input class="form-control" name="q" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off">
<div class="input-group-append">
<button class="btn btn-outline-primary" type="submit"><i class="fas fa-search"></i></button>
</div>
</div>
</form>
</ul>
@endauth

View file

@ -12,10 +12,11 @@
@endsection
@push('meta')<meta property="og:description" content="{{$profile->bio}}">
<meta property="og:image" content="{{$profile->avatarUrl()}}">
<link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on PixelFed" type="application/atom+xml">
@if(false == $settings['crawlable'] || $profile->remote_url)
<meta name="robots" content="noindex, nofollow">
@else <meta property="og:image" content="{{$profile->avatarUrl()}}">
<link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on PixelFed" type="application/atom+xml">
<link href='{{$profile->permalink()}}' rel='alternate' type='application/activity+json'>
@endif
@endpush

View file

@ -0,0 +1,5 @@
@extends('layouts.app')
@section('content')
@endsection

View file

@ -1,10 +1,12 @@
@extends('layouts.app')
@section('content')
<search-results></search-results>
<search-results query="{{request()->query('q')}}" profile-id="{{Auth::user()->profile->id}}"></search-results>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
<script type="text/javascript" src="{{mix('js/search.js')}}"></script>
<script type="text/javascript">
new Vue({
el: '#content'

View file

@ -22,11 +22,157 @@
<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 href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
</head>
<body class="">
<main id="content">
<landing-page></landing-page>
<section class="container">
<div class="row py-5 mb-5">
<div class="col-12 col-md-6 d-none d-md-block">
<div class="m-md-4" style="position: absolute; transform: scale(0.66)">
<div class="marvel-device note8" style="position: absolute;z-index:10;">
<div class="inner"></div>
<div class="overflow">
<div class="shadow"></div>
</div>
<div class="speaker"></div>
<div class="sensors"></div>
<div class="more-sensors"></div>
<div class="sleep"></div>
<div class="volume"></div>
<div class="camera"></div>
<div class="screen">
<img src="/img/landing/android_1.jpg" class="img-fluid">
</div>
</div>
<div class="marvel-device iphone-x" style="position: absolute;z-index: 20;margin: 99px 0 0 151px;">
<div class="notch">
<div class="camera"></div>
<div class="speaker"></div>
</div>
<div class="top-bar"></div>
<div class="sleep"></div>
<div class="bottom-bar"></div>
<div class="volume"></div>
<div class="overflow">
<div class="shadow shadow--tr"></div>
<div class="shadow shadow--tl"></div>
<div class="shadow shadow--br"></div>
<div class="shadow shadow--bl"></div>
</div>
<div class="inner-shadow"></div>
<div class="screen">
<div id="iosDevice">
<img v-if="!loading" src="/img/landing/ios_4.jpg" class="img-fluid">
<img v-if="!loading" src="/img/landing/ios_3.jpg" class="img-fluid">
<img v-if="!loading" src="/img/landing/ios_2.jpg" class="img-fluid">
<img src="/img/landing/ios_1.jpg" class="img-fluid">
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-5 offset-md-1">
<div>
<div class="card my-4">
<div class="card-body px-lg-5">
<div class="text-center pt-3">
<img src="/img/pixelfed-icon-color.svg">
</div>
<div class="py-3 text-center">
<h3 class="font-weight-bold">Pixelfed</h3>
<p class="mb-0 lead">Photo sharing for everyone</p>
</div>
<div>
@if(true === config('pixelfed.open_registration'))
<form class="px-1" method="POST" action="{{ route('register') }}">
@csrf
<div class="form-group row">
<div class="col-md-12">
<input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" placeholder="{{ __('Name') }}" required autofocus>
@if ($errors->has('name'))
<span class="invalid-feedback">
<strong>{{ $errors->first('name') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required>
@if ($errors->has('username'))
<span class="invalid-feedback">
<strong>{{ $errors->first('username') }}</strong>
</span>
@endif
</div>
</div>
<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="{{ __('E-Mail Address') }}" required>
@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">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
</div>
</div>
@if(config('pixelfed.recaptcha'))
<div class="row my-3">
{!! Recaptcha::render() !!}
</div>
@endif
<div class="form-group row">
<div class="col-md-12">
<button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
{{ __('Register') }}
</button>
</div>
</div>
<p class="mb-0 font-weight-bold text-lighter small">By signing up, you agree to our <a href="/site/terms" class="text-muted">Terms of Use</a> and <a href="/site/privacy" class="text-muted">Privacy Policy</a>.</p>
</form>
@else
<div style="min-height: 350px" class="d-flex justify-content-center align-items-center">
<div class="text-center">
<p class="lead">Registrations are closed.</p>
<p class="text-lighter small">You can find a list of other instances on <a href="https://the-federation.info/pixelfed" class="text-muted font-weight-bold">the-federation.info/pixelfed</a> or <a href="https://fediverse.network/pixelfed" class="text-muted font-weight-bold">fediverse.network/pixelfed</a></p>
</div>
</div>
@endif
</div>
</div>
</div>
<div class="card card-body">
<p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container py-3">
@ -38,13 +184,9 @@
<a href="{{route('site.privacy')}}" class="text-primary pr-3">{{__('site.privacy')}}</a>
<a href="{{route('site.platform')}}" class="text-primary pr-3">API</a>
<a href="{{route('site.language')}}" class="text-primary pr-3">{{__('site.language')}}</a>
<a href="https://pixelfed.org" class="text-muted float-right" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">Powered by PixelFed</a>
<a href="https://pixelfed.org" class="text-muted float-right" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">Powered by Pixelfed</a>
</p>
</div>
</footer>
</body>
<script type="text/javascript" src="{{mix('js/app.js')}}"></script>
<script type="text/javascript" src="{{mix('js/landing.js')}}"></script>
</html>

View file

@ -130,6 +130,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('media/preview/{profileId}/{mediaId}', 'ApiController@showTempMedia')->name('temp-media');
Route::get('results', 'SearchController@results');
Route::post('visibility', 'StatusController@toggleVisibility');
Route::group(['prefix' => 'report'], function () {
Route::get('/', 'ReportController@showForm')->name('report.form');

View file

@ -8,13 +8,6 @@ use Illuminate\Foundation\Testing\WithoutMiddleware;
class InstalledTest extends TestCase
{
/** @test */
public function landing_page()
{
$response = $this->get('/');
$response->assertStatus(200);
}
/** @test */
public function nodeinfo_api()
{

9
webpack.mix.js vendored
View file

@ -28,16 +28,19 @@ mix.js('resources/assets/js/app.js', 'public/js')
// Timeline component
.js('resources/assets/js/timeline.js', 'public/js')
// LandingPage component
.js('resources/assets/js/landing.js', 'public/js')
// ComposeModal component
.js('resources/assets/js/compose.js', 'public/js')
// SearchResults component
.js('resources/assets/js/search.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css', {
implementation: require('node-sass')
})
.sass('resources/assets/sass/appdark.scss', 'public/css', {
implementation: require('node-sass')
})
.sass('resources/assets/sass/landing.scss', 'public/css', {
implementation: require('node-sass')
})
.version();