Skip to content

Commit

Permalink
User Search Feature (#153)
Browse files Browse the repository at this point in the history
* start implementing search bar and dashboard

* implemented user search logic with search dashboard

* Added API implementation

* Added tests for search

* styling search for mobile

* added pagination, fixed design

* Completed User Search
- moved validation from front to backend
- fixed swagger-doc
- fixed tests
- added simple pagination

* Fixed Codacy gods' requirements

* removed datasources

* whoops. URLs are hard

* remove unused includes.

* Fixed some things.

* Removed auth::check

* für Kris. <3

Co-authored-by: Thomas Englert <[email protected]>
  • Loading branch information
HerrLevin and Thomas Englert authored Nov 11, 2020
1 parent 42008d6 commit 5d020cc
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Homestead.json
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
Expand Down
90 changes: 78 additions & 12 deletions api-swagger-v0.yml
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,66 @@ paths:
$ref: '#/components/responses/BadRequestError'
406:
$ref: '#/components/responses/GDPRError'
/user/search/{searchQuery}:
get:
security:
- bearerAuth: []
tags: [User profile]
summary: Searches for a user
parameters:
- in: path
name: searchQuery
schema:
type: string
example: 'jdo'
required: true
description: The query for the user search.
responses:
200:
description: OK.
content:
application/json:
schema:
type: object
properties:
current_page:
type: integer
example: 1
data:
type: array
items:
$ref: '#/components/schemas/User'
first_page_url:
type: string
example: "https://traewelling.de/api/v0/user/search/jdo?page=1"
from:
type: integer
example: 1
description: Frist element on page
next_page_url:
type: string
example: "https://traewelling.de/api/v0/user/search/jdo?page=2"
nullable: true
path:
type: string
example: "https://traewelling.de/api/v0/user/search/jdo"
per_page:
type: integer
example: 5
prev_page_url:
type: string
example: null
nullable: true
to:
type: integer
example: 5
description: Last element on page
400:
$ref: '#/components/responses/BadRequestError'
401:
$ref: '#/components/responses/UnauthorizedError'
406:
$ref: '#/components/responses/GDPRError'
/getuser:
get:
security:
Expand Down Expand Up @@ -1194,18 +1254,18 @@ components:
$ref: '#/components/schemas/Status'
first_page_url:
type: string
example: "https://api.traewelling.de/v0/statuses?page=1"
example: "https://traewelling.de/api/v0/statuses?page=1"
from:
type: integer
example: 1
description: Frist element on page
next_page_url:
type: string
example: "https://api.traewelling.de/v0/statuses?page=2"
example: "https://traewelling.de/api/v0/statuses?page=2"
nullable: true
path:
type: string
example: "https://api.traewelling.de/v0/statuses"
example: "https://traewelling.de/api/v0/statuses"
per_page:
type: integer
example: 15
Expand Down Expand Up @@ -1245,18 +1305,18 @@ components:
$ref: '#/components/schemas/User'
first_page_url:
type: string
example: "https://api.traewelling.de/v0/statuses/1/likes?page=1"
example: "https://traewelling.de/api/v0/statuses/1/likes?page=1"
from:
type: integer
example: 1
description: Frist element on page
next_page_url:
type: string
example: "https://api.traewelling.de/v0/statuses/1/likes?page=2"
example: "https://traewelling.de/api/v0/statuses/1/likes?page=2"
nullable: true
path:
type: string
example: "https://api.traewelling.de/v0/statuses"
example: "https://traewelling.de/api/v0/statuses"
per_page:
type: integer
example: 15
Expand Down Expand Up @@ -1328,15 +1388,21 @@ components:
type: string
example: "jdoe"
train_distance:
type: number
type: string
format: float
example: 454.59
example: "454.59"
train_duration:
type: integer
example: 317
type: string
format: integer
example: "317"
points:
type: integer
example: 66
type: string
format: integer
example: "66"
averageSpeed:
type: number
format: float
example: 100.5678954
db_rest_stopover:
type: object
properties:
Expand Down
4 changes: 4 additions & 0 deletions app/Http/Controllers/API/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ public function getLeaderboard(Request $request) {
'kilometers' => $leaderboardResponse['kilometers']
]);
}

public function searchUser($searchQuery) {
return UserBackend::searchUser($searchQuery);
}
}
16 changes: 13 additions & 3 deletions app/Http/Controllers/FrontendUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Http\Controllers\UserController as UserBackend;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Gd\Commands\BackupCommand;
use Symfony\Component\HttpKernel\Exception\HttpException;

class FrontendUserController extends Controller
{
Expand Down Expand Up @@ -96,4 +94,16 @@ public function updateProfilePicture(Request $request)
$profilePictureResponse = UserBackend::updateProfilePicture($avatar);
return response()->json($profilePictureResponse);
}

public function searchUser(Request $request) {
try {
$userSearchResponse = UserBackend::searchUser($request['searchQuery']);
} catch (HttpException $exception) {
return redirect()->back();
}

return view("search", [
'userSearchResponse' => $userSearchResponse
]);
}
}
35 changes: 24 additions & 11 deletions app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public static function getProfilePicture($username)
$hex = dechex($hash & 0x00FFFFFF);

$picture = Image::canvas(512, 512, $hex)
->insert(public_path('/img/user.png'))
->encode('png')->getEncoded();
->insert(public_path('/img/user.png'))
->encode('png')->getEncoded();
$ext = 'png';
}

Expand Down Expand Up @@ -235,11 +235,11 @@ public static function getProfilePage($username) {
return null;
}
$statuses = $user->statuses()->with('user',
'trainCheckin',
'trainCheckin.Origin',
'trainCheckin.Destination',
'trainCheckin.HafasTrip',
'event')->orderBy('created_at', 'DESC')->paginate(15);
'trainCheckin',
'trainCheckin.Origin',
'trainCheckin.Destination',
'trainCheckin.HafasTrip',
'event')->orderBy('created_at', 'DESC')->paginate(15);


$twitterUrl = "";
Expand All @@ -252,8 +252,8 @@ public static function getProfilePage($username) {
if ($mastodonServer != null) {
$mastodonDomain = $mastodonServer->domain;
$mastodonAccountInfo = Mastodon::domain($mastodonDomain)
->token($user->socialProfile->mastodon_token)
->get("/accounts/" . $user->socialProfile->mastodon_id);
->token($user->socialProfile->mastodon_token)
->get("/accounts/" . $user->socialProfile->mastodon_id);
$mastodonUrl = $mastodonAccountInfo["url"];
}
} catch (Exception $e) {
Expand Down Expand Up @@ -379,8 +379,8 @@ public static function getLeaderboard() {
public static function registerByDay(Carbon $date)
{
return User::where("created_at", ">=", $date->copy()->startOfDay())
->where("created_at", "<=", $date->copy()->endOfDay())
->count();
->where("created_at", "<=", $date->copy()->endOfDay())
->count();
}

public static function updateDisplayName($displayname)
Expand All @@ -396,4 +396,17 @@ public static function updateDisplayName($displayname)
$user->name = $displayname;
$user->save();
}

public static function searchUser(?string $searchQuery) {
$validator = Validator::make(['searchQuery' => $searchQuery], ['searchQuery' => 'required|alpha_num']);
if ($validator->fails()) {
abort(400);
}

return User::where(
'name', 'like', "%{$searchQuery}%"
)->orWhere(
'username', 'like', "%{$searchQuery}%"
)->simplePaginate(10);
}
}
3 changes: 2 additions & 1 deletion resources/lang/de/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"always-dbl" => "Mit #dbl twittern?",
"liked-status" => "gefällt dieser Status.",
"liked-own-status" => "gefällt der eigene Status.",
"invalid-mastodon" => ":domain scheint keine Mastodon-Instanz zu sein."
"invalid-mastodon" => ":domain scheint keine Mastodon-Instanz zu sein.",
"no-user" => "Keinen Nutzer gefunden."
];
3 changes: 2 additions & 1 deletion resources/lang/en/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"always-dbl" => "Tweet with #dbl?",
"liked-status" => "likes this status.",
"liked-own-status" => "likes their own status.",
"invalid-mastodon" => ":domain doesn't seem to be a Mastodon instance."
"invalid-mastodon" => ":domain doesn't seem to be a Mastodon instance.",
"no-user" => "No user found."
];
1 change: 1 addition & 0 deletions resources/sass/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ $cyan: #6cb2eb;
$grey: rgba(0, 0, 0, 0.125);
$bahnrot: rgb(192, 57, 43);
$text-color: #212529;
$blueCounterColor: rgb(0, 46, 102);
1 change: 1 addition & 0 deletions resources/sass/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ body {
@import "./components/eventspage";
@import "./components/notifications-board";
@import "./components/leaderboard";
@import "./components/search";
4 changes: 4 additions & 0 deletions resources/sass/components/image-box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
border-radius: 50%;
}
}

.search-image-box {
display: block !important;
}
3 changes: 3 additions & 0 deletions resources/sass/components/search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.navbar.navbar-dark form .md-form input:focus:not([readonly]) {
border-color: $blueCounterColor;
}
9 changes: 9 additions & 0 deletions resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@
</li>
@endif
@else
<form class="form-inline" action="{{ route('userSearch') }}">
<div class="input-group md-form form-sm form-2 pl-0 m-0">
<input name="searchQuery" class="border border-white rounded-left form-control my-0 py-1" type="text" placeholder="Search" aria-label="User suchen">
<div class="input-group-append">
<button class="input-group-text btn-primary lighten-2" id="basic-text1" type="submit"><i class="fas fa-search text-grey"
aria-hidden="true"></i></button>
</div>
</div>
</form>
<li class="nav-item d-none d-md-inline-block">
<a href="#" id="notifications-toggle" class="nav-link" data-toggle="modal" data-target="#notifications-board">
<span class="notifications-bell far fa-bell"></span>
Expand Down
63 changes: 63 additions & 0 deletions resources/views/search.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@extends('layouts.app')

@section('title')
Dashboard
@endsection

@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
@if($userSearchResponse->count() == 0)
<div class="col-md-8 offset-md-2">
<div class="alert my-3 alert-danger" role="alert">
{{ __('user.no-user') }}
</div>
</div>
@endif
@foreach($userSearchResponse as $user)
<div class="card status mt-3">
<div class="card-body row">
<div class="col-2 image-box search-image-box d-lg-flex">
<a href="{{ route('account.show', ['username' => $user->username]) }}">
<img src="{{ route('account.showProfilePicture', ['username' => $user->username]) }}" alt="profile picture">
</a>
</div>

<div class="col pl-0">
<a href="{{ route('account.show', ['username' => $user->username]) }}">
<h4>{{ $user->name }} <small>{{ $user->username }}</small></h4>
</a>
<h6>
<small>
<span class="font-weight-bold"><i class="fa fa-route d-inline"></i>&nbsp;{{ $user->train_distance }}</span><span class="small font-weight-lighter">km</span>
<span class="font-weight-bold pl-sm-2"><i class="fa fa-stopwatch d-inline"></i>&nbsp;{!! durationToSpan(secondsToDuration($user->train_duration * 60)) !!}</span>
<span class="font-weight-bold pl-sm-2"><i class="fa fa-dice-d20 d-inline"></i>&nbsp;{{ $user->points }}</span><span class="small font-weight-lighter">{{__('profile.points-abbr')}}</span>
</small>
</h6>
@if($user->id !== Auth::user()->id)
@if(Auth::user()->follows->where('id', $user->id)->first() === null)
<a href="#" class="btn btn-sm btn-primary follow" data-userid="{{ $user->id }}" data-following="no">{{__('profile.follow')}}</a>
@else
<a href="#" class="btn btn-sm btn-danger follow" data-userid="{{ $user->id }}" data-following="yes">{{__('profile.unfollow')}}</a>
@endif
<script>
window.translFollow = "{{__('profile.follow')}}";
window.translUnfollow = "{{__('profile.unfollow')}}";
</script>
@else
<a href="{{ route('settings') }}" class="btn btn-sm btn-primary">{{ __('profile.settings') }}</a>
@endif
</div>
</div>
</div>
@endforeach
</div>
</div>
<div class="row justify-content-center mt-5">
{{ $userSearchResponse->withQueryString()->links() }}
</div>
@include('includes.edit-modal')
@include('includes.delete-modal')
</div><!--- /container -->
@endsection
1 change: 1 addition & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
Route::group(['prefix' => 'user'], function() {
Route::get('leaderboard', 'API\UserController@getLeaderboard')->name('api.v0.user.leaderboard');
Route::get('{username}', 'API\UserController@show')->name('api.v0.user');
Route::get('search/{query}', 'API\UserController@searchUser')->name('api.v0.user.search');
Route::get('{username}/active', 'API\UserController@active')->name('api.v0.user.active');
Route::put('profilepicture', 'API\UserController@PutProfilepicture')->name('api.v0.user.profilepicture');
Route::put('displayname', 'API\UserController@PutDisplayname')->name('api.v0.user.displayname');
Expand Down
3 changes: 3 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,7 @@

Route::post('/notifications/readAll', [NotificationController::class, 'readAll'])
->name('notifications.readAll');

Route::get('/search/', [FrontendUserController::class, 'searchUser'])
->name('userSearch');
});
Loading

0 comments on commit 5d020cc

Please sign in to comment.