diff --git a/.idea/olm.iml b/.idea/olm.iml new file mode 100644 index 000000000..e69de29bb diff --git a/app/Helpers/Get/User/GetUserDataHelper.php b/app/Helpers/Get/User/GetUserDataHelper.php new file mode 100644 index 000000000..89d723bf2 --- /dev/null +++ b/app/Helpers/Get/User/GetUserDataHelper.php @@ -0,0 +1,50 @@ +', $userXp)->count() + 1; + + // Todo - Store this metadata in Redis + $totalPhotosAllUsers = Photo::count(); + // Todo - Store this metadata in Redis + $totalTagsAllUsers = Photo::sum('total_litter'); // this doesn't include brands + + $usersTotalTags = $totalTags; + + $photoPercent = ($totalImages && $totalPhotosAllUsers) ? number_format(($totalImages / $totalPhotosAllUsers), 2) : 0; + $tagPercent = ($usersTotalTags && $totalTagsAllUsers) ? number_format(($usersTotalTags / $totalTagsAllUsers), 2) : 0; + + // XP needed to reach the next level + $nextLevelXp = Level::where('xp', '>=', $userXp)->first()->xp; + $requiredXp = $nextLevelXp - $userXp; + + return [ + 'totalUsers' => $totalUsers, + 'usersPosition' => $usersPosition, + 'tagPercent' => $tagPercent, + 'photoPercent' => $photoPercent, + 'requiredXp' => $requiredXp + ]; + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 7e059374c..2c5121da2 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -// use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class HomeController extends Controller @@ -18,15 +17,19 @@ public function index () $auth = Auth::check(); $user = null; + if ($auth) { $user = Auth::user(); + + // Load this data $user->roles; + $user->settings; } // We set this to true when user verifies their email $verified = false; - // or when a user unsubscribes from emails + // We set this to true when a user unsubscribes from communication $unsub = false; return view('root', compact('auth', 'user', 'verified', 'unsub')); diff --git a/app/Http/Controllers/User/ProfileController.php b/app/Http/Controllers/User/ProfileController.php index 6fed66a9e..827788832 100644 --- a/app/Http/Controllers/User/ProfileController.php +++ b/app/Http/Controllers/User/ProfileController.php @@ -2,14 +2,19 @@ namespace App\Http\Controllers\User; -use App\Exports\CreateCSVExport; -use App\Http\Controllers\Controller; -use App\Jobs\EmailUserExportCompleted; +use App\Helpers\Get\User\GetUserDataHelper; use App\Level; use App\Models\Photo; use App\Models\User\User; + +use App\Exports\CreateCSVExport; +use App\Jobs\EmailUserExportCompleted; + +use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Auth; +use App\Http\Controllers\Controller; + class ProfileController extends Controller { /** @@ -91,6 +96,7 @@ public function geojson () 'features' => [] ]; + // Might be big... $photos = $query->get(); // Populate geojson object @@ -100,7 +106,7 @@ public function geojson () 'type' => 'Feature', 'geometry' => [ 'type' => 'Point', - 'coordinates' => [$photo->lon, $photo->lat] + 'coordinates' => [$photo->lat, $photo->lon] ], 'properties' => [ @@ -118,45 +124,47 @@ public function geojson () } return [ + 'success' => true, 'geojson' => $geojson ]; } /** - * Get the total number of users, and the current users position - * To get the current position, we need to count how many users have more XP than current users + * Load extra data for the Users Profile Page + * + * This is also used to get extra data for a PublicProfile if allowed. + * + * Gets: + * - total number of users, + * - users position * * @return array */ - public function index () + public function index () : array { - $user = Auth::user(); - - // Todo - Store this metadata in another table - $totalUsers = User::count(); - - $usersPosition = $user->position; + $user = request()->has('username') + ? User::where('username', request()->username)->first() + : Auth::user(); - // Todo - Store this metadata in Redis - $totalPhotosAllUsers = Photo::count(); - // Todo - Store this metadata in Redis - $totalTagsAllUsers = Photo::sum('total_litter'); // this doesn't include brands - - $usersTotalTags = $user->total_tags; - - $photoPercent = ($user->total_images && $totalPhotosAllUsers) ? ($user->total_images / $totalPhotosAllUsers) : 0; - $tagPercent = ($usersTotalTags && $totalTagsAllUsers) ? ($usersTotalTags / $totalTagsAllUsers) : 0; + if (Auth::guest() && (!$user || !isset($user->settings) || !$user->settings->show_public_profile)) { + return [ + 'success' => false + ]; + } - // XP needed to reach the next level - $nextLevelXp = Level::where('xp', '>=', $user->xp)->first()->xp; - $requiredXp = $nextLevelXp - $user->xp; + $userData = GetUserDataHelper::get( + $user->xp, + $user->total_tags, + $user->total_images + ); return [ - 'totalUsers' => $totalUsers, - 'usersPosition' => $usersPosition, - 'tagPercent' => $tagPercent, - 'photoPercent' => $photoPercent, - 'requiredXp' => $requiredXp + 'success' => true, + 'totalUsers' => $userData->totalUsers, + 'usersPosition' => $userData->usersPosition, + 'tagPercent' => $userData->tagPercent, + 'photoPercent' => $userData->photoPercent, + 'requiredXp' => $userData->requiredXp ]; } } diff --git a/app/Http/Controllers/User/PublicProfile/PublicProfileMapController.php b/app/Http/Controllers/User/PublicProfile/PublicProfileMapController.php new file mode 100644 index 000000000..13e65a2f9 --- /dev/null +++ b/app/Http/Controllers/User/PublicProfile/PublicProfileMapController.php @@ -0,0 +1,85 @@ +with(['settings' => function ($q) { + $q->select('id', 'user_id', 'public_profile_show_map', 'show_public_profile'); + }]) + ->where([ + 'username' => request()->username + ])->first(); + + if (!$user || !isset($user->settings) || !$user->settings->show_public_profile || !$user->settings->public_profile_show_map) { + return redirect('/'); + } + + // Todo - Pre-cluster each users photos + $query = Photo::select('id', 'filename', 'datetime', 'lat', 'lon', 'model', 'result_string', 'created_at') + ->where([ + 'user_id' => $user->id, + ['verified', '>=', 2] + ]) + ->whereDate(request()->period, '>=', request()->start) + ->whereDate(request()->period, '<=', request()->end); + + $geojson = [ + 'type' => 'FeatureCollection', + 'features' => [] + ]; + + // Might be big... + $photos = $query->get(); + + // Populate geojson object + foreach ($photos as $photo) + { + $feature = [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'Point', + 'coordinates' => [$photo->lat, $photo->lon] + ], + + 'properties' => [ + 'img' => $photo->filename, + 'model' => $photo->model, + 'datetime' => $photo->datetime, + 'latlng' => [$photo->lat, $photo->lon], + 'text' => $photo->result_string + ] + ]; + + array_push($geojson["features"], $feature); + } + + return [ + 'success' => true, + 'geojson' => $geojson + ]; + } +} diff --git a/app/Http/Controllers/User/Settings/PublicProfileController.php b/app/Http/Controllers/User/Settings/PublicProfileController.php new file mode 100644 index 000000000..a250bb8a3 --- /dev/null +++ b/app/Http/Controllers/User/Settings/PublicProfileController.php @@ -0,0 +1,131 @@ +with(['settings' => function ($q) { + $q->select('id', 'user_id', 'public_profile_show_map', 'public_profile_download_my_data', 'show_public_profile'); + }]) + ->where([ + 'username' => $username + ])->first(); + + if (!$user || !isset($user->settings) || !$user->settings->show_public_profile) { + return redirect('/'); + } + + // Extra user data + // totalUsers, usersPosition, tagPercent, photoPercent, requiredXp + $userData = GetUserDataHelper::get( + $user->xp, + $user->total_tags, + $user->total_images + ); + + return view('root')->with([ + 'auth' => Auth::check(), + 'success' => true, + 'user' => Auth::user(), + 'verified' => null, + 'unsub' => null, + 'username' => $username, + 'publicProfile' => $user, + 'userData' => json_encode($userData) + ]); + } + + /** + * Change the privacy status of a users Public Profile setting + * + * @param Request $request + * + * @return array + */ + public function toggle (Request $request): array + { + try + { + $user = Auth::user(); + + $settings = UserSettings::firstOrCreate(['user_id' => $user->id]); + + $settings->show_public_profile = ($settings->wasRecentlyCreated) + ? true + : $settings->show_public_profile = ! $settings->show_public_profile; + + $settings->save(); + + return [ + 'success' => true, + 'settings' => $settings + ]; + } + catch (\Exception $e) + { + \Log::info(['PublicProfileController@toggle', $e->getMessage()]); + + return ['success' => false]; + } + } + + /** + * Update the settings on the users public profile + * + * @param Request $request + * + * @return array + * + * Todo: + * - add validation + * - only update what params were changed + */ + public function update (Request $request): array + { + try + { + $user = Auth::user(); + + $user->settings->public_profile_download_my_data = $request->download ?: false; + $user->settings->public_profile_show_map = $request->map ?: false; + $user->settings->save(); + + return [ + 'success' => true + ]; + } + catch (\Exception $e) + { + \Log::info(['PublicProfileController@update', $e->getMessage()]); + + return [ + 'success' => false + ]; + } + } +} diff --git a/app/Http/Controllers/User/Settings/SocialMediaController.php b/app/Http/Controllers/User/Settings/SocialMediaController.php new file mode 100644 index 000000000..743796fbc --- /dev/null +++ b/app/Http/Controllers/User/Settings/SocialMediaController.php @@ -0,0 +1,41 @@ +settings->twitter = $request->twitter; + $user->settings->instagram = $request->instagram; + $user->settings->save(); + + return [ + 'success' => true + ]; + } + catch (\Exception $e) + { + \Log::info(['SocialMediaController@update', $e->getMessage()]); + + return [ + 'success' => false + ]; + } + } +} diff --git a/app/Listeners/AddTags/CompileResultsString.php b/app/Listeners/AddTags/CompileResultsString.php index c863d1f70..16413ebb6 100644 --- a/app/Listeners/AddTags/CompileResultsString.php +++ b/app/Listeners/AddTags/CompileResultsString.php @@ -14,8 +14,8 @@ class CompileResultsString implements ShouldQueue * We save the metadata on the photos table as a string to speed up page load * and avoid additional requests * - * When a record exists, we apply the translation key => value, - * for every item in each category. + * When a litter tag exists, we generate the translation key => value, + * for every tag in each category. * * with this 1 line of code! * diff --git a/app/Listeners/AddTags/UpdateUser.php b/app/Listeners/AddTags/UpdateUser.php index c8156495c..2039d3b8d 100644 --- a/app/Listeners/AddTags/UpdateUser.php +++ b/app/Listeners/AddTags/UpdateUser.php @@ -27,7 +27,8 @@ public function handle (TagsVerifiedByAdmin $event) if ($user->count_correctly_verified >= 100) { - $user->littercoin_allowance += 1; + // $user->littercoin_allowance += 1; + $user->littercoin_owed += 1; $user->count_correctly_verified = 0; event (new LittercoinMined($user->id, '100-images-verified')); diff --git a/app/Models/User/User.php b/app/Models/User/User.php index f90d75fbe..ee52dcae8 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -90,7 +90,8 @@ public static function boot () 'previous_tags', 'remaining_teams', 'photos_per_month', - 'bbox_verification_count' + 'bbox_verification_count', + 'show_public_profile' ]; /** @@ -225,6 +226,14 @@ public function setPasswordAttribute ($password) $this->attributes['password'] = bcrypt($password); } + /** + * The users settings if they exist + */ + public function settings () + { + return $this->hasOne('App\Models\User\UserSettings'); + } + /** * Has Many Through relationships */ diff --git a/app/Models/User/UserSettings.php b/app/Models/User/UserSettings.php index c3d91af60..8f9618f71 100644 --- a/app/Models/User/UserSettings.php +++ b/app/Models/User/UserSettings.php @@ -8,16 +8,26 @@ class UserSettings extends Model { protected $fillable = [ 'user_id', - 'picked_up', - 'global_flag', - 'previous_tags', - 'litter_picked_up', - 'show_name_maps', - 'show_username_maps', - 'show_name_leaderboard', - 'show_username_leaderboard', - 'show_name_createdby', - 'show_username_createdby', - 'email_sub' + + 'show_public_profile', + 'public_profile_download_my_data', + 'public_profile_show_map', + 'twitter', + 'instagram', + 'link_username' + +// We need to move these columns here from users table +// Some of the column names could be improved +// 'picked_up', +// 'global_flag', +// 'previous_tags', +// 'litter_picked_up', +// 'show_name_maps', +// 'show_username_maps', +// 'show_name_leaderboard', +// 'show_username_leaderboard', +// 'show_name_createdby', +// 'show_username_createdby', +// 'email_sub' ]; } diff --git a/database/migrations/2020_09_13_114634_create_user_settings_table.php b/database/migrations/2020_09_13_114634_create_user_settings_table.php deleted file mode 100644 index 8b6a197a2..000000000 --- a/database/migrations/2020_09_13_114634_create_user_settings_table.php +++ /dev/null @@ -1,45 +0,0 @@ -id(); - // $table->integer('user_id'); - // $table->foreign('user_id')->references('id')->on('users'); - - // $table->boolean('picked_up')->default(0); - // $table->string('global_flag')->nullable(); - // $table->boolean('previous_tags')->default(0); - - // $table->boolean('litter_picked_up')->default(0); - // $table->boolean('show_name_maps')->default(0); - // $table->boolean('show_username_maps')->default(0); - // $table->boolean('show_name_leaderboard')->default(0); - // $table->boolean('show_username_leaderboard')->default(0); - // $table->boolean('show_name_createdby')->default(0); - // $table->boolean('show_username_createdby')->default(0); - // $table->timestamps(); - // }); - // } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('user_settings'); - } -} diff --git a/database/migrations/2021_08_14_171118_create_user_settings_table.php b/database/migrations/2021_08_14_171118_create_user_settings_table.php new file mode 100644 index 000000000..9c4f7b484 --- /dev/null +++ b/database/migrations/2021_08_14_171118_create_user_settings_table.php @@ -0,0 +1,43 @@ +id(); + $table->unsignedInteger('user_id'); + $table->foreign('user_id')->references('id')->on('users'); + + $table->boolean('show_public_profile')->default(false); + $table->boolean('public_profile_download_my_data')->default(false); + $table->boolean('public_profile_show_map')->default(false); + + $table->string('twitter')->nullable()->default(null); + $table->string('instagram')->nullable()->default(null); + + $table->string('link_username')->nullable()->default(null); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_settings'); + } +} diff --git a/public/css/openlittermap.css b/public/css/openlittermap.css index 89eec0b5e..aa0cf2223 100644 --- a/public/css/openlittermap.css +++ b/public/css/openlittermap.css @@ -115,11 +115,23 @@ html { } .flex { - display: flex; + display: flex !important; +} + +.flex-0-25 { + flex: 0.25em; } .flex-1 { - flex: 1; + flex: 1 !important; +} + +.flex-1-15 { + flex: 1.15 !important; +} + +.flex-1-25 { + flex: 1.25 !important; } .fullheight { @@ -337,6 +349,14 @@ html { color: #363636 !important; } +.w-3 { + width: 3em; +} + +.w-15 { + width: 15em; +} + .w10 { width: 10em; } diff --git a/resources/js/components/Charts/CategoriesPieChart.vue b/resources/js/components/Charts/CategoriesPieChart.vue new file mode 100644 index 000000000..f51cd81fe --- /dev/null +++ b/resources/js/components/Charts/CategoriesPieChart.vue @@ -0,0 +1,69 @@ + diff --git a/resources/js/components/Charts/Radar.vue b/resources/js/components/Charts/Radar.vue index 8f69247f7..50ea32286 100644 --- a/resources/js/components/Charts/Radar.vue +++ b/resources/js/components/Charts/Radar.vue @@ -1,15 +1,17 @@ diff --git a/resources/js/components/Profile/middle/ProfileCalendar.vue b/resources/js/components/Profile/middle/ProfileCalendar.vue index 3e7250f5d..aaf46aecc 100644 --- a/resources/js/components/Profile/middle/ProfileCalendar.vue +++ b/resources/js/components/Profile/middle/ProfileCalendar.vue @@ -1,5 +1,5 @@ @@ -27,29 +34,22 @@ import { FunctionalCalendar } from 'vue-functional-calendar'; export default { name: 'ProfileCalendar', - components: { FunctionalCalendar }, + components: { + FunctionalCalendar + }, data () { return { - btn: 'button long-purp', calendarData: {}, period: 'created_at', periods: [ 'created_at', 'datetime' - ] + ], + processing: false } }, computed: { - - /** - * Add spinner when processing - */ - button () - { - return this.processing ? this.btn + ' is-loading' : this.btn; - }, - /** * Return true to disable the button */ @@ -65,7 +65,6 @@ export default { } }, methods: { - /** * Get map data */ @@ -73,11 +72,15 @@ export default { { if (this.disabled) return; + this.processing = true; + await this.$store.dispatch('GET_USERS_PROFILE_MAP_DATA', { period: this.period, start: this.calendarData.dateRange.start, - end: this.calendarData.dateRange.end, + end: this.calendarData.dateRange.end }); + + this.processing = false; }, /** @@ -85,7 +88,7 @@ export default { */ getPeriod (period) { - if (! period) period = this.period; + if (!period) period = this.period; return this.$t('teams.dashboard.times.' + period) }, @@ -95,8 +98,13 @@ export default { diff --git a/resources/js/components/Profile/middle/ProfileCategories.vue b/resources/js/components/Profile/middle/ProfileCategories.vue index 61b7f6bce..5bed6e3c6 100644 --- a/resources/js/components/Profile/middle/ProfileCategories.vue +++ b/resources/js/components/Profile/middle/ProfileCategories.vue @@ -1,44 +1,50 @@ @@ -109,19 +103,24 @@ export default { diff --git a/resources/js/components/Profile/top/ProfileNextTarget.vue b/resources/js/components/Profile/top/ProfileNextTarget.vue index 879337431..8e266a6f5 100644 --- a/resources/js/components/Profile/top/ProfileNextTarget.vue +++ b/resources/js/components/Profile/top/ProfileNextTarget.vue @@ -1,59 +1,35 @@ diff --git a/resources/js/components/Profile/top/ProfileStats.vue b/resources/js/components/Profile/top/ProfileStats.vue index e05543e90..a2bc4a6e8 100644 --- a/resources/js/components/Profile/top/ProfileStats.vue +++ b/resources/js/components/Profile/top/ProfileStats.vue @@ -1,43 +1,37 @@ + + diff --git a/resources/js/views/Settings/SocialMediaIntegration.vue b/resources/js/views/Settings/SocialMediaIntegration.vue new file mode 100644 index 000000000..e1e4a2392 --- /dev/null +++ b/resources/js/views/Settings/SocialMediaIntegration.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/resources/js/views/general/Profile.vue b/resources/js/views/general/Profile.vue index 7d9bcccd0..a84b94384 100644 --- a/resources/js/views/general/Profile.vue +++ b/resources/js/views/general/Profile.vue @@ -1,41 +1,64 @@ diff --git a/resources/views/root.blade.php b/resources/views/root.blade.php index 227d782a7..b5a726f53 100644 --- a/resources/views/root.blade.php +++ b/resources/views/root.blade.php @@ -5,5 +5,8 @@ user="{{ $user }}" verified="{{ $verified }}" unsub="{{ $unsub }}" + username="{{ isset($username) ? $username : false }}" + public-profile="{{ isset($publicProfile) ? $publicProfile : null }}" + user-data="{{ isset($userData) ? $userData : null }}" > @stop diff --git a/routes/web.php b/routes/web.php index b5ab4bb3e..fb4d2d94c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -92,7 +92,7 @@ // Tag litter to an image Route::get('tag', 'HomeController@index'); -// The users profile +// The authenticated users profile Route::get('profile', 'HomeController@index'); // Get unverified paginated photos for tagging @@ -121,21 +121,15 @@ // Delete selected photos Route::post('/user/profile/photos/delete', 'User\UserPhotoController@destroy'); +// The users profile. Used for Authenticated User and PublicProfile +Route::get('/user/profile/index', 'User\ProfileController@index'); +Route::get('/user/profile/map', 'User\ProfileController@geojson'); +Route::get('/user/profile/download', 'User\ProfileController@download'); + /** * USER SETTINGS */ -Route::get('/settings', 'HomeController@index'); -Route::get('/settings/password', 'HomeController@index'); -Route::get('/settings/details', 'HomeController@index'); -Route::get('/settings/account', 'HomeController@index'); -Route::get('/settings/payments', 'HomeController@index'); -Route::get('/settings/privacy', 'HomeController@index'); -Route::get('/settings/littercoin', 'HomeController@index'); -Route::get('/settings/phone', 'HomeController@index'); -Route::get('/settings/presence', 'HomeController@index'); -Route::get('/settings/email', 'HomeController@index'); -Route::get('/settings/show-flag', 'HomeController@index'); -Route::get('/settings/teams', 'HomeController@index'); +Route::get('/settings/{route?}', 'HomeController@index'); // Game settings @ SettingsController // Toggle Presense of a piece of litter @@ -185,6 +179,13 @@ // Save Country Flag for top 10 Route::post('/settings/save-flag', 'SettingsController@saveFlag'); +// Public Profile +Route::post('/settings/public-profile/toggle', 'User\Settings\PublicProfileController@toggle'); +Route::post('/settings/public-profile/update', 'User\Settings\PublicProfileController@update'); + +// Social Media +Route::post('/settings/social-media/update', 'User\Settings\SocialMediaController@update'); + // Teams Route::get('/teams', 'HomeController@index'); Route::get('/teams/get-types', 'Teams\TeamsController@types'); @@ -207,11 +208,6 @@ Route::post('/teams/download', 'Teams\TeamsController@download'); Route::post('/teams/leaderboard/visibility', 'Teams\TeamsLeaderboardController@toggle')->middleware('auth'); -// The users profile -Route::get('/user/profile/index', 'User\ProfileController@index'); -Route::get('/user/profile/map', 'User\ProfileController@geojson'); -Route::get('/user/profile/download', 'User\ProfileController@download'); - // Unsubscribe via email (user not authenticated) Route::get('/emails/unsubscribe/{token}', 'EmailSubController@unsubEmail'); Route::get('/unsubscribe/{token}', 'UsersController@unsubscribeEmail'); @@ -326,3 +322,9 @@ Route::get('/verify/index', 'Bbox\VerifyBoxController@index'); Route::post('/verify/update', 'Bbox\VerifyBoxController@update'); }); + +// Public Profile Routes +Route::get('/user/public-profile/map', 'User\PublicProfile\PublicProfileMapController@index'); + +// Visit public profile of a User with settings on +Route::get('{username}', 'User\Settings\PublicProfileController@index');