1
0
Fork 0

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 1464 additions and 3204 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

@ -35,8 +35,8 @@ class FederationController extends Controller
{
$this->authCheck();
$this->validate($request, [
'acct' => 'required|string|min:3|max:255',
]);
'acct' => 'required|string|min:3|max:255',
]);
$acct = $request->input('acct');
$nickname = Nickname::normalizeProfileUrl($acct);
@ -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,37 +73,46 @@ 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;
}
$users = Profile::select('username', 'name', 'id')
->whereNull('status')
->whereNull('domain')
->where('username', 'like', '%'.$tag.'%')
//->orWhere('remote_url', $tag)
->limit(20)
->get();
if($users->count() > 0) {
$profiles = $users->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'id' => $item->id
];
});
$tokens->push($profiles);
}
return $tokens;
});
$users = Profile::select('username', 'name', 'id')
->whereNull('status')
->where('id', '!=', Auth::user()->profile->id)
->where('username', 'like', '%'.$tag.'%')
->orWhere('remote_url', $tag)
->limit(20)
->get();
if($users->count() > 0) {
$profiles = $users->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => $item->id,
'entity' => [
'id' => $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'thumb' => $item->avatarUrl()
]
];
});
if(isset($tokens['profiles'])) {
array_push($tokens['profiles'], $profiles);
} else {
$tokens['profiles'] = $profiles;
}
}
$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',
/*
|--------------------------------------------------------------------------

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
public/css/landing.css vendored Normal file

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/search.js vendored Normal file

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,14 @@
{
"/js/activity.js": "/js/activity.js?id=7915246c3bc2b7e9770e",
"/js/app.js": "/js/app.js?id=a51da95c2b9df7cf8de2",
"/css/app.css": "/css/app.css?id=20dc108f4dbbc76c9827",
"/css/appdark.css": "/css/appdark.css?id=08d0a92912b101bb7457",
"/js/components.js": "/js/components.js?id=793c50b76d7f9028f550",
"/js/compose.js": "/js/compose.js?id=aff84cf29322124685cf",
"/js/app.js": "/js/app.js?id=3cd2f5b91d50cb028347",
"/css/app.css": "/css/app.css?id=cc8780fa2f1c0e8156cc",
"/css/appdark.css": "/css/appdark.css?id=c98702344aa5c1af2dda",
"/css/landing.css": "/css/landing.css?id=36c1fca7fbdc4cdf5e7c",
"/js/components.js": "/js/components.js?id=ae830db50efb9df181df",
"/js/compose.js": "/js/compose.js?id=e988873b96c16cd9b764",
"/js/discover.js": "/js/discover.js?id=75fb12b06ee23fa05186",
"/js/landing.js": "/js/landing.js?id=328c8bec8bde1e516430",
"/js/profile.js": "/js/profile.js?id=69fd7039c50660f4e34d",
"/js/status.js": "/js/status.js?id=a8289051dc914ddc013f",
"/js/timeline.js": "/js/timeline.js?id=c6a7953a0751f12efe0d"
"/js/profile.js": "/js/profile.js?id=be1a00c4bf3ad5449ba9",
"/js/search.js": "/js/search.js?id=df9027d746934eb442f0",
"/js/status.js": "/js/status.js?id=305d8cb21b4c817bbcb7",
"/js/timeline.js": "/js/timeline.js?id=3f82696a8664e306a8d0"
}

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">
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
<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}}
</p>
<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>
</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();