From d53a04e7e158a30b80c59ed5473f2fcfee06ea3a Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 9 Dec 2022 05:52:15 -0800 Subject: [PATCH 01/44] Add news announcement model and transformer --- app/Models/NewsAnnouncement.php | 69 +++++++++++++++++++ .../NewsAnnouncementTransformer.php | 29 ++++++++ ...12_09_100000_create_news_announcements.php | 37 ++++++++++ .../lib/interfaces/news-announcement.ts | 15 ++++ 4 files changed, 150 insertions(+) create mode 100644 app/Models/NewsAnnouncement.php create mode 100644 app/Transformers/NewsAnnouncementTransformer.php create mode 100644 database/migrations/2022_12_09_100000_create_news_announcements.php create mode 100644 resources/assets/lib/interfaces/news-announcement.ts diff --git a/app/Models/NewsAnnouncement.php b/app/Models/NewsAnnouncement.php new file mode 100644 index 00000000000..5f280c4caf9 --- /dev/null +++ b/app/Models/NewsAnnouncement.php @@ -0,0 +1,69 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +namespace App\Models; + +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Builder; + +/** + * @property-read string|null $content_html + * @property string|null $content_markdown + * @property Carbon|null $ended_at + * @property-read string|null $ended_at_json + * @property int $id + * @property string $image_url + * @property int $order + * @property Carbon $started_at + * @property-read string $started_at_json + * @property string $url + * @method static Builder default() + */ +class NewsAnnouncement extends Model +{ + public $incrementing = false; + public $timestamps = false; + + protected $dates = ['ended_at', 'started_at']; + + public function scopeDefault(Builder $query): void + { + $now = Carbon::now(); + + $query + ->where('started_at', '<=', $now) + ->where('ended_at', '>', $now) + ->orWhereNull('ended_at') + ->orderBy('order'); + } + + public function getAttribute($key) + { + return match ($key) { + 'content_markdown', + 'id', + 'image_url', + 'order', + 'url' => $this->getRawAttribute($key), + + 'ended_at', + 'started_at' => $this->getTimeFast($key), + + 'ended_at_json', + 'started_at_json' => $this->getJsonTimeFast($key), + + 'content_html' => $this->getContentHtml(), + }; + } + + private function getContentHtml(): ?string + { + return $this->content_markdown === null + ? null + : markdown($this->content_markdown); + } +} diff --git a/app/Transformers/NewsAnnouncementTransformer.php b/app/Transformers/NewsAnnouncementTransformer.php new file mode 100644 index 00000000000..e8a8c6c5be9 --- /dev/null +++ b/app/Transformers/NewsAnnouncementTransformer.php @@ -0,0 +1,29 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +namespace App\Transformers; + +use App\Models\NewsAnnouncement; + +class NewsAnnouncementTransformer extends TransformerAbstract +{ + public function transform(NewsAnnouncement $newsAnnouncement): array + { + return [ + 'content' => [ + 'html' => $newsAnnouncement->content_html, + 'markdown' => $newsAnnouncement->content_markdown, + ], + 'ended_at' => $newsAnnouncement->ended_at_json, + 'id' => $newsAnnouncement->getKey(), + 'image_url' => $newsAnnouncement->image_url, + 'order' => $newsAnnouncement->order, + 'started_at' => $newsAnnouncement->started_at_json, + 'url' => $newsAnnouncement->url, + ]; + } +} diff --git a/database/migrations/2022_12_09_100000_create_news_announcements.php b/database/migrations/2022_12_09_100000_create_news_announcements.php new file mode 100644 index 00000000000..c980386f707 --- /dev/null +++ b/database/migrations/2022_12_09_100000_create_news_announcements.php @@ -0,0 +1,37 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('news_announcements', function (Blueprint $table): void { + $table->mediumIncrements('id'); + $table->text('content_markdown')->nullable(); + $table->timestamp('ended_at')->nullable(); + $table->string('image_url'); + $table->tinyInteger('order'); + $table->timestamp('started_at')->useCurrent(); + $table->string('url'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news_announcements'); + } +}; diff --git a/resources/assets/lib/interfaces/news-announcement.ts b/resources/assets/lib/interfaces/news-announcement.ts new file mode 100644 index 00000000000..d2246e952a6 --- /dev/null +++ b/resources/assets/lib/interfaces/news-announcement.ts @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +export default interface NewsAnnouncement { + content: { + html: string | null; + markdown: string | null; + }; + ended_at: string | null; + id: number; + image_url: string; + order: number; + started_at: string; + url: string; +} From b253dd23e53de14c68c7ea44a9da44a4ceff3edf Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 9 Dec 2022 05:52:16 -0800 Subject: [PATCH 02/44] Add news announcement carousel thing to dashboard --- app/Http/Controllers/HomeController.php | 6 + resources/assets/less/bem-index.less | 2 + .../assets/less/bem/news-announcement.less | 29 +++ .../assets/less/bem/news-announcements.less | 61 ++++++ .../lib/entrypoints/news-announcements.tsx | 11 ++ .../assets/lib/news-announcements/main.tsx | 176 ++++++++++++++++++ resources/views/home/user.blade.php | 12 ++ 7 files changed, 297 insertions(+) create mode 100644 resources/assets/less/bem/news-announcement.less create mode 100644 resources/assets/less/bem/news-announcements.less create mode 100644 resources/assets/lib/entrypoints/news-announcements.tsx create mode 100644 resources/assets/lib/news-announcements/main.tsx diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index b3bbb99b522..18db149a719 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -12,6 +12,7 @@ use App\Models\BeatmapDownload; use App\Models\Beatmapset; use App\Models\Forum\Post; +use App\Models\NewsAnnouncement; use App\Models\NewsPost; use App\Models\UserDonation; use Auth; @@ -72,11 +73,16 @@ public function index() if (Auth::check()) { $newBeatmapsets = Beatmapset::latestRanked(); + $newsAnnouncements = json_collection( + NewsAnnouncement::default()->get(), + 'NewsAnnouncement', + ); $popularBeatmapsets = Beatmapset::popular()->get(); return ext_view('home.user', compact( 'newBeatmapsets', 'news', + 'newsAnnouncements', 'popularBeatmapsets' )); } else { diff --git a/resources/assets/less/bem-index.less b/resources/assets/less/bem-index.less index e5e911710b8..da13548da2e 100644 --- a/resources/assets/less/bem-index.less +++ b/resources/assets/less/bem-index.less @@ -233,6 +233,8 @@ @import "bem/navbar-mobile-before"; @import "bem/navbar-mobile-item"; @import "bem/navbar-mobile-search"; +@import "bem/news-announcement"; +@import "bem/news-announcements"; @import "bem/news-card"; @import "bem/news-index"; @import "bem/news-post-preview"; diff --git a/resources/assets/less/bem/news-announcement.less b/resources/assets/less/bem/news-announcement.less new file mode 100644 index 00000000000..4761b7c341d --- /dev/null +++ b/resources/assets/less/bem/news-announcement.less @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.news-announcement { + flex-shrink: 0; + overflow: hidden; + position: relative; + + &__content { + background: hsla(var(--hsl-b4), 0.5); + bottom: 0; + padding: 10px; + position: absolute; + text-align: center; + width: 100%; + } + + &__image { + height: 100%; + left: 50%; + position: absolute; + transform: translateX(-50%); + } + + &__link { + display: block; + height: 100%; + } +} diff --git a/resources/assets/less/bem/news-announcements.less b/resources/assets/less/bem/news-announcements.less new file mode 100644 index 00000000000..90c4357e9ad --- /dev/null +++ b/resources/assets/less/bem/news-announcements.less @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.news-announcements { + margin-top: 20px; + position: relative; + + &__announcements-container { + display: flex; + height: 250px; + overflow: hidden; + } + + &__button { + .reset-input(); + padding: 10px; + position: absolute; + top: 50%; + transform: translateY(-50%); + + &:hover { + color: @osu-colour-l1; + } + + &--left { + --icon: @fa-var-chevron-left; + left: 0; + } + + &--right { + --icon: @fa-var-chevron-right; + right: 0; + } + + &::before { + .fas(); + content: var(--icon); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + } + } + + &__indicator { + background: @osu-colour-h1; + border-radius: 10000px; + height: 2px; + width: 30px; + + &--active { + height: 6px; + } + } + + &__indicators { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + margin-top: 10px; + } +} diff --git a/resources/assets/lib/entrypoints/news-announcements.tsx b/resources/assets/lib/entrypoints/news-announcements.tsx new file mode 100644 index 00000000000..092807e4545 --- /dev/null +++ b/resources/assets/lib/entrypoints/news-announcements.tsx @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import Main from 'news-announcements/main'; +import core from 'osu-core-singleton'; +import * as React from 'react'; +import { parseJson } from 'utils/json'; + +core.reactTurbolinks.register('news-announcements', () => ( +
+)); diff --git a/resources/assets/lib/news-announcements/main.tsx b/resources/assets/lib/news-announcements/main.tsx new file mode 100644 index 00000000000..86822b5b1ae --- /dev/null +++ b/resources/assets/lib/news-announcements/main.tsx @@ -0,0 +1,176 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import Img2x from 'components/img2x'; +import NewsAnnouncement from 'interfaces/news-announcement'; +import { action, autorun, IReactionDisposer, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { classWithModifiers } from 'utils/css'; + +const bn = 'news-announcements'; +const itemBn = 'news-announcement'; + +interface Props { + announcements: NewsAnnouncement[]; +} + +@observer +export default class Main extends React.Component { + @observable private announcementWidth = 0; + @observable private readonly containerRef = React.createRef(); + @observable private index = 0; + private registerResizeObserverDisposer?: IReactionDisposer; + private resizeObserver?: ResizeObserver; + private rotateAnnouncementTimer?: NodeJS.Timer; + @observable private readonly topRef = React.createRef(); + + private get length() { + return this.props.announcements.length; + } + + constructor(props: Props) { + super(props); + + makeObservable(this); + } + + componentDidMount() { + this.setRotateAnnouncementTimer(); + + this.registerResizeObserverDisposer = autorun(() => { + if (this.topRef.current != null) { + this.resizeObserver?.disconnect(); + this.resizeObserver?.observe(this.topRef.current); + } + }); + this.resizeObserver = new ResizeObserver(action((entries) => { + if (entries.length > 0) { + this.announcementWidth = entries[0].contentRect.width; + + setImmediate(() => { + if (this.containerRef.current != null) { + this.containerRef.current.scrollLeft = this.announcementWidth * this.index; + } + }); + } + })); + } + + componentWillUnmount() { + this.clearRotateAnnouncementTimer(); + this.registerResizeObserverDisposer?.(); + this.resizeObserver?.disconnect(); + } + + render() { + if (this.length === 0) { + return null; + } + + return ( + <> +
+
+ {this.props.announcements.map((announcement) => ( +
+ + + + {announcement.content.html != null && ( +
+ )} +
+ ))} +
+ {this.renderButtons()} +
+ {this.renderIndicators()} + + ); + } + + private clearRotateAnnouncementTimer = () => { + if (this.rotateAnnouncementTimer != null) { + clearInterval(this.rotateAnnouncementTimer); + + this.rotateAnnouncementTimer = undefined; + } + }; + + private handleButtonClick = (event: React.MouseEvent) => { + this.setIndex(parseInt(event.currentTarget.dataset.index ?? '', 10)); + }; + + private renderButtons() { + if (this.length > 1) { + return ( + <> +