diff --git a/packages/app/src/pages/music/components/explore/index.jsx b/packages/app/src/pages/music/components/explore/index.jsx index 1ab23aa1..46fd3270 100755 --- a/packages/app/src/pages/music/components/explore/index.jsx +++ b/packages/app/src/pages/music/components/explore/index.jsx @@ -3,11 +3,10 @@ import * as antd from "antd" import classnames from "classnames" import { Translation } from "react-i18next" +import Image from "components/Image" import Searcher from "components/Searcher" import { Icons, createIconRender } from "components/Icons" -import { WithPlayerContext } from "contexts/WithPlayerContext" - import FeedModel from "models/feed" import MusicModel from "models/music" @@ -16,6 +15,48 @@ import PlaylistItem from "components/Music/PlaylistItem" import "./index.less" +const FeaturedPlaylist = (props) => { + const [featuredPlaylist, setFeaturedPlaylist] = React.useState(false) + + const onClick = () => { + if (!featuredPlaylist) { + return + } + + app.navigation.goToPlaylist(featuredPlaylist.playlist_id) + } + + React.useEffect(() => { + MusicModel.getFeaturedPlaylists().then((data) => { + if (data[0]) { + console.log(`Loaded featured playlist >`, data[0]) + setFeaturedPlaylist(data[0]) + } + }) + }, []) + + if (!featuredPlaylist) { + return null + } + + return
+ + +
+

{featuredPlaylist.title}

+

{featuredPlaylist.description}

+ + { + featuredPlaylist.genre &&
+ {featuredPlaylist.genre} +
+ } +
+
+} + const MusicNavbar = (props) => { return
- { - groupsKeys.map((key, index) => { - const decorator = ResultGroupsDecorators[key] ?? { - icon: null, - label: key, - renderItem: () => null - } + { + groupsKeys.map((key, index) => { + const decorator = ResultGroupsDecorators[key] ?? { + icon: null, + label: key, + renderItem: () => null + } - return
-
-

- { - createIconRender(decorator.icon) - } - - {(t) => t(decorator.label)} - -

-
- -
+ return
+
+

{ - data[key].map((item, index) => { - return decorator.renderItem({ - key: index, - item - }) - }) + createIconRender(decorator.icon) } -

+ + {(t) => t(decorator.label)} + +
- }) - } + +
+ { + data[key].map((item, index) => { + return decorator.renderItem({ + key: index, + item + }) + }) + } +
+
+ }) + }
} @@ -287,6 +328,8 @@ export default (props) => { { !searchResults &&
+ + } diff --git a/packages/app/src/pages/music/components/explore/index.less b/packages/app/src/pages/music/components/explore/index.less index caff093b..086cb8d0 100755 --- a/packages/app/src/pages/music/components/explore/index.less +++ b/packages/app/src/pages/music/components/explore/index.less @@ -1,7 +1,6 @@ html { &.mobile { .musicExplorer { - .playlistExplorer_section_list { overflow: visible; overflow-x: scroll; @@ -16,6 +15,101 @@ html { } } +.featured_playlist { + position: relative; + + display: flex; + flex-direction: row; + + overflow: hidden; + + background-color: var(--background-color-accent); + + width: 100%; + min-height: 200px; + height: fit-content; + + border-radius: 12px; + + cursor: pointer; + + &:hover { + .featured_playlist_content { + + h1, + p { + -webkit-text-stroke-width: 1.6px; + -webkit-text-stroke-color: var(--border-color); + + color: var(--background-color-contrast); + } + } + + .lazy-load-image-background { + opacity: 1; + } + } + + .lazy-load-image-background { + z-index: 50; + + position: absolute; + + opacity: 0.3; + + transition: all 300ms ease-in-out !important; + + img { + width: 100%; + height: 100%; + } + } + + .featured_playlist_content { + z-index: 55; + + padding: 20px; + + display: flex; + flex-direction: column; + + h1 { + font-size: 2.5rem; + font-family: "Space Grotesk", sans-serif; + font-weight: 900; + + transition: all 300ms ease-in-out !important; + } + + p { + font-size: 1rem; + font-family: "Space Grotesk", sans-serif; + + transition: all 300ms ease-in-out !important; + } + + .featured_playlist_genre { + z-index: 55; + + position: absolute; + + left: 0; + bottom: 0; + + margin: 10px; + + background-color: var(--background-color-accent); + border: 1px solid var(--border-color); + + border-radius: 12px; + + padding: 10px 20px; + } + } + + +} + .music_navbar { display: flex; flex-direction: column; diff --git a/packages/comty.js/src/models/music/index.js b/packages/comty.js/src/models/music/index.js index f12cafff..bff3c5cb 100644 --- a/packages/comty.js/src/models/music/index.js +++ b/packages/comty.js/src/models/music/index.js @@ -7,6 +7,21 @@ export default class MusicModel { return globalThis.__comty_shared_state.instances["music"] } + /** + * Retrieves the official featured playlists. + * + * @return {Promise} The data containing the featured playlists. + */ + static async getFeaturedPlaylists() { + const response = await request({ + instance: MusicModel.api_instance, + method: "GET", + url: "/featured/playlists", + }) + + return response.data + } + /** * Retrieves track data for a given ID. * diff --git a/packages/music_server/src/controllers/featured/index.js b/packages/music_server/src/controllers/featured/index.js new file mode 100644 index 00000000..1044cbb0 --- /dev/null +++ b/packages/music_server/src/controllers/featured/index.js @@ -0,0 +1,21 @@ +import path from "path" +import createRoutesFromDirectory from "@utils/createRoutesFromDirectory" +import getMiddlewares from "@utils/getMiddlewares" + +export default async (router) => { + // create a file based router + const routesPath = path.resolve(__dirname, "routes") + + const middlewares = await getMiddlewares(["withOptionalAuth"]) + + for (const middleware of middlewares) { + router.use(middleware) + } + + router = createRoutesFromDirectory("routes", routesPath, router) + + return { + path: "/featured", + router, + } +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/featured/routes/get/playlists.js b/packages/music_server/src/controllers/featured/routes/get/playlists.js new file mode 100644 index 00000000..206f633a --- /dev/null +++ b/packages/music_server/src/controllers/featured/routes/get/playlists.js @@ -0,0 +1,19 @@ +import { FeaturedPlaylist } from "@shared-classes/DbModels" + +export default async (req, res) => { + const includeDisabled = req.query["include-disabled"] === "true" + + const query = { + enabled: true + } + + if (includeDisabled) { + query.enabled = undefined + } + + let playlists = await FeaturedPlaylist.find(query).catch((error) => { + return [] + }) + + return res.json(playlists) +} \ No newline at end of file diff --git a/packages/server/src/index.js b/packages/server/src/index.js index 8a2f28c7..51bc575d 100755 --- a/packages/server/src/index.js +++ b/packages/server/src/index.js @@ -1,3 +1,5 @@ +global.FORCE_ENV = "prod" + import Boot from "linebridge/bootstrap" import { Server } from "linebridge/dist/server" diff --git a/shared/classes/DbModels/featuredPlaylist/index.js b/shared/classes/DbModels/featuredPlaylist/index.js new file mode 100644 index 00000000..48eacb7d --- /dev/null +++ b/shared/classes/DbModels/featuredPlaylist/index.js @@ -0,0 +1,12 @@ +export default { + name: "FeaturedPlaylist", + collection: "featuredPlaylists", + schema: { + title: { type: String, required: true }, + description: { type: String }, + cover_url: { type: String }, + enabled: { type: Boolean, default: true }, + genre: { type: String }, + playlist_id: { type: String, required: true }, + } +} \ No newline at end of file