Lots of cool new stuff

This commit is contained in:
Gamebrary 2022-10-19 22:22:35 -07:00
parent c4f662da66
commit 439916cf30
41 changed files with 2104 additions and 622 deletions

View file

@ -1,5 +1,4 @@
// firebase emulators:start --only functions // firebase emulators:start --only functions
// TODO: Inject token using axios middleware
// Add json object in .runtimeconfig.json to use env variables locally // Add json object in .runtimeconfig.json to use env variables locally
const functions = require('firebase-functions'); const functions = require('firebase-functions');

View file

@ -0,0 +1,506 @@
{
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&relatedSite={relatedSite?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
},
"queries": {
"request": [
{
"title": "Google Custom Search - Most Watched Games on Twitch twitchtracker",
"totalResults": "227000",
"searchTerms": "Most Watched Games on Twitch twitchtracker",
"count": 10,
"startIndex": 1,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "25c3c6c97e0db4365"
}
],
"nextPage": [
{
"title": "Google Custom Search - Most Watched Games on Twitch twitchtracker",
"totalResults": "227000",
"searchTerms": "Most Watched Games on Twitch twitchtracker",
"count": 10,
"startIndex": 11,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "25c3c6c97e0db4365"
}
]
},
"context": {
"title": "gamebrary"
},
"searchInformation": {
"searchTime": 0.515786,
"formattedSearchTime": "0.52",
"totalResults": "227000",
"formattedTotalResults": "227,000"
},
"items": [
{
"kind": "customsearch#result",
"title": "Most Watched Games on Twitch, October 2022 · TwitchTracker",
"htmlTitle": "<b>Most Watched Games on Twitch</b>, October 2022 · <b>TwitchTracker</b>",
"link": "https://twitchtracker.com/games",
"displayLink": "twitchtracker.com",
"snippet": "Most Watched Games on Twitch ; #1. Just Chatting. 315K ; #2. Overwatch 2. 273K ; #3. League of Legends. 218K ; #4. Grand Theft Auto V · 113K ; #5. Counter-Strike: ...",
"htmlSnippet": "<b>Most Watched Games on Twitch</b> ; #1. Just Chatting. 315K ; #2. Overwatch 2. 273K ; #3. League of Legends. 218K ; #4. Grand Theft Auto V &middot; 113K ; #5. Counter-Strike:&nbsp;...",
"cacheId": "Txl-67EDxH0J",
"formattedUrl": "https://twitchtracker.com/games",
"htmlFormattedUrl": "https://<b>twitchtracker</b>.com/<b>games</b>",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSEvgEyyzCMnZfi_KDj9q7HcADBRt6eZgPo5_M78JgoaQsngLtPtUuJNy7D",
"width": "275",
"height": "183"
}
],
"metatags": [
{
"og:image": "https://twitchtracker.com/img/share-tt.png",
"theme-color": "#ffffff",
"twitter:card": "summary_large_image",
"og:type": "website",
"twitter:site": "@twitchtracker_",
"og:site_name": "TwitchTracker",
"viewport": "width=device-width, initial-scale=1.0",
"og:title": "Most Watched Games on Twitch, October 2022",
"og:locale": "en_US",
"og:url": "https://twitchtracker.com/games",
"og:description": "Ranked by average concurrent viewers for the last 7 days, October 2022"
}
],
"cse_image": [
{
"src": "https://twitchtracker.com/img/share-tt.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Most Watched Games on Twitch | Esports Content and Total",
"htmlTitle": "<b>Most Watched Games on Twitch</b> | Esports Content and Total",
"link": "https://newzoo.com/insights/rankings/top-games-twitch",
"displayLink": "newzoo.com",
"snippet": "Explore the ranking below to find the most popular games on Twitch across the top channels that broadcast game-related content. Each game's position in the ...",
"htmlSnippet": "Explore the ranking below to find the <b>most popular games on Twitch</b> across the top channels that broadcast game-related content. Each game&#39;s position in the&nbsp;...",
"cacheId": "pw0bWHtq5yEJ",
"formattedUrl": "https://newzoo.com/insights/rankings/top-games-twitch",
"htmlFormattedUrl": "https://newzoo.com/insights/rankings/<b>top</b>-<b>games</b>-<b>twitch</b>",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSQSOKzf4Z2TsxKZ7nBT2w32mQueu4DtyWf3CGfG0Gnl78MukQnu_hmlVM",
"width": "300",
"height": "168"
}
],
"metatags": [
{
"og:image": "https://newzoo.com/wp-content/uploads/2022/02/Rankings_MostWatchedonTwitch.jpg",
"og:type": "article",
"og:image:width": "1920",
"twitter:card": "summary",
"og:site_name": "Newzoo",
"og:title": "Most Watched Games on Twitch | Esports Content and Total",
"og:image:height": "1080",
"twitter:label1": "Est. reading time",
"og:image:type": "image/jpeg",
"og:description": "Most viewed games on Twitch every month, including the watched esports content and overall content. Click to see the ranking!",
"article:publisher": "https://www.facebook.com/NewzooHQ/",
"twitter:data1": "1 minute",
"facebook-domain-verification": "5azu56bev83xm26g6x5d8s0xe1kkxy",
"twitter:site": "@NewzooHQ",
"article:modified_time": "2022-05-20T08:11:21+00:00",
"viewport": "width=device-width, initial-scale=1.0",
"og:locale": "en_US",
"og:url": "https://newzoo.com/insights/rankings/top-games-twitch"
}
],
"cse_image": [
{
"src": "https://newzoo.com/wp-content/uploads/2022/02/Rankings_MostWatchedonTwitch.jpg"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Twitch Usage and Growth Statistics: How Many People Use Twitch ...",
"htmlTitle": "<b>Twitch</b> Usage and Growth Statistics: How Many People Use <b>Twitch</b> ...",
"link": "https://backlinko.com/twitch-users",
"displayLink": "backlinko.com",
"snippet": "Jan 5, 2022 ... Most popular Twitch channels; Most popular Twitch games; Twitch by the money. Let's get right into it: Twitch statistics (Top Picks). Twitch ...",
"htmlSnippet": "Jan 5, 2022 <b>...</b> <b>Most popular Twitch</b> channels; <b>Most popular Twitch games</b>; <b>Twitch</b> by the money. Let&#39;s get right into it: <b>Twitch</b> statistics (<b>Top</b> Picks). <b>Twitch</b>&nbsp;...",
"cacheId": "Id-xSK1XCEcJ",
"formattedUrl": "https://backlinko.com/twitch-users",
"htmlFormattedUrl": "https://backlinko.com/<b>twitch</b>-users",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcSm9HjdDrDlDhMyWycw_5H0IzujUND6QHoZSuIeZBlFurrdk0PXpBOz31I",
"width": "311",
"height": "162"
}
],
"metatags": [
{
"application-name": "Backlinko",
"msapplication-tilecolor": "#2b5797",
"og:image": "https://api.backlinko.com/app/uploads/2018/07/twitch-users-post-banner.png",
"theme-color": "#ffffff",
"og:type": "article",
"og:image:width": "1600",
"article:published_time": "2021-01-26T09:15:48-05:00",
"twitter:card": "summary_large_image",
"og:site_name": "Backlinko",
"apple-mobile-web-app-title": "Backlinko",
"og:title": "Twitch Usage and Growth Statistics: How Many People Use Twitch in 2022?",
"og:image:height": "837",
"bingbot": "index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1",
"og:description": "In-depth look at the user base of the popular video streaming platform, Twitch.",
"twitter:creator": "@Backlinko",
"article:publisher": "https://www.facebook.com/Backlinko",
"twitter:image": "https://api.backlinko.com/app/uploads/2018/07/twitch-users-post-banner.png",
"next-head-count": "36",
"twitter:site": "@Backlinko",
"article:modified_time": "2022-01-05T10:52:02+00:00",
"viewport": "width=device-width, initial-scale=1, shrink-to-fit=no",
"og:locale": "en_US",
"og:url": "https://backlinko.com/twitch-users"
}
],
"cse_image": [
{
"src": "https://api.backlinko.com/app/uploads/2018/07/twitch-users-post-banner.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "10 years of Twitch: Looking back on the top games | Nerd Street",
"htmlTitle": "10 years of <b>Twitch</b>: Looking back on the <b>top games</b> | Nerd Street",
"link": "https://nerdstreet.com/news/2021/6/twitch-10-year-anniversary-top-games-by-year",
"displayLink": "nerdstreet.com",
"snippet": "Jun 7, 2021 ... Emmett Shear, who would go on to become the CEO, that's what he was watching the most.” In June 2021, Starcraft 2 was the 57th most watched game ...",
"htmlSnippet": "Jun 7, 2021 <b>...</b> Emmett Shear, who would go on to become the CEO, that&#39;s what he was <b>watching</b> the <b>most</b>.” In June 2021, Starcraft 2 was the 57th <b>most watched game</b>&nbsp;...",
"cacheId": "3-WPbSYXJjYJ",
"formattedUrl": "https://nerdstreet.com/news/.../twitch-10-year-anniversary-top-games-by-year",
"htmlFormattedUrl": "https://nerdstreet.com/news/.../<b>twitch</b>-10-year-anniversary-<b>top</b>-<b>games</b>-by-year",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRuBdjA9GdF1iLdweUs8D7KOrC_Uj_Xf6OgkdlbM7wUgnOdzFekkSsEqjw",
"width": "300",
"height": "168"
}
],
"metatags": [
{
"msapplication-tilecolor": "#da532c",
"og:image": "https://cdn.sanity.io/images/zoz4y99f/production/c1889cfcfd99d10cd31e148119c23983e806db44-1600x900.png",
"theme-color": "#000000",
"twitter:title": "10 years of Twitch: Looking back on the top games | Nerd Street",
"og:type": "article",
"twitter:card": "summary_large_image",
"og:site_name": "Nerd Street",
"og:title": "10 years of Twitch: Looking back on the top games",
"og:article:publish_time": "2021-06-07T16:19:56Z",
"og:description": "From being the go-to site for League of Legends esports to seeing the rise of the battle royale genre, heres a look back on the top games throughout Twitchs first decade.",
"twitter:creator": "@Mitch_Reames",
"next-head-count": "25",
"twitter:site": "@nerdstgamers",
"viewport": "initial-scale=1.0, width=device-width",
"og:url": "https://nerdstreet.com/news/2021/6/twitch-10-year-anniversary-top-games-by-year",
"og:article:author": "Mitch Reames"
}
],
"cse_image": [
{
"src": "https://cdn.sanity.io/images/zoz4y99f/production/c1889cfcfd99d10cd31e148119c23983e806db44-1600x900.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "SullyGnome: Twitch Stats and Analytics - Games & Channels",
"htmlTitle": "SullyGnome: <b>Twitch</b> Stats and Analytics - <b>Games</b> &amp; Channels",
"link": "https://sullygnome.com/",
"displayLink": "sullygnome.com",
"snippet": "Twitch stats and analytics for your favorite streamers and games. View the fastest growing channels and most popular games.",
"htmlSnippet": "<b>Twitch</b> stats and analytics for your favorite streamers and <b>games</b>. View the fastest growing channels and <b>most popular games</b>.",
"cacheId": "7_sU_Z2qmOcJ",
"formattedUrl": "https://sullygnome.com/",
"htmlFormattedUrl": "https://sullygnome.com/",
"pagemap": {
"metatags": [
{
"viewport": "width=device-width, initial-scale=1.0"
}
]
}
},
{
"kind": "customsearch#result",
"title": "50+ Twitch Statistics for Content Creators in 2022 | Uscreen",
"htmlTitle": "50+ <b>Twitch</b> Statistics for Content Creators in 2022 | Uscreen",
"link": "https://www.uscreen.tv/blog/twitch-statistics/",
"displayLink": "www.uscreen.tv",
"snippet": "Jun 30, 2022 ... (TwitchTracker); In 2022, people have already watched over 6 ... The most popular game played on Twitch live streams is League of Legends.",
"htmlSnippet": "Jun 30, 2022 <b>...</b> (<b>TwitchTracker</b>); In 2022, people have already <b>watched</b> over 6 ... The <b>most popular game</b> played on <b>Twitch</b> live streams is League of Legends.",
"cacheId": "0QtmwNPFIxkJ",
"formattedUrl": "https://www.uscreen.tv/blog/twitch-statistics/",
"htmlFormattedUrl": "https://www.uscreen.tv/blog/<b>twitch</b>-statistics/",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcSs_ox-gFivRoK_sE6P214g2zG9Dtdt4f7BmWie8XS_gup5PruD_TEQ77E",
"width": "297",
"height": "170"
}
],
"metatags": [
{
"p:domain_verify": "53aef21ad69a7ee8ba0300f0f20a28a1",
"og:image": "https://www.uscreen.tv/wp-content/uploads/2022/06/twitch-statistics-hero.jpg",
"og:type": "article",
"article:published_time": "2022-06-30T09:41:19+00:00",
"og:image:width": "1050",
"twitter:card": "summary_large_image",
"og:site_name": "Uscreen",
"og:title": "50+ Twitch Statistics for Content Creators in 2022",
"og:image:height": "600",
"twitter:label1": "Written by",
"twitter:label2": "Est. reading time",
"og:image:type": "image/jpeg",
"msapplication-tileimage": "https://www.uscreen.tv/wp-content/uploads/2018/04/favico.png",
"og:description": "Pore over 50+ Twitch statistics you should know as a content creator in 2022.",
"twitter:creator": "@uscreentv",
"article:publisher": "https://www.facebook.com/uscreentv/",
"twitter:data1": "Parris Johnson",
"twitter:data2": "10 minutes",
"ahrefs-site-verification": "eca3e5350dd3e828ec60185a42d51d53976ff03dd3582abda3d2616f5d520995",
"twitter:site": "@uscreentv",
"article:modified_time": "2022-10-04T07:35:32+00:00",
"viewport": "width=device-width, initial-scale=1",
"og:locale": "en_US",
"og:url": "https://www.uscreen.tv/blog/twitch-statistics/"
}
],
"cse_image": [
{
"src": "https://www.uscreen.tv/wp-content/uploads/2022/06/twitch-statistics-hero.jpg"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Most watched games on Twitch - SullyGnome",
"htmlTitle": "<b>Most watched games on Twitch</b> - SullyGnome",
"link": "https://sullygnome.com/games/30/watched",
"displayLink": "sullygnome.com",
"snippet": "Statistics the most watched games on Twitch in the past 30 days. ... Most watched · Most streamed · Peak viewers · Peak channels · Viewer distribution.",
"htmlSnippet": "Statistics the <b>most watched games on Twitch</b> in the past 30 days. ... Most watched &middot; Most streamed &middot; Peak viewers &middot; Peak channels &middot; Viewer distribution.",
"cacheId": "8nUywrr01sQJ",
"formattedUrl": "https://sullygnome.com/games/30/watched",
"htmlFormattedUrl": "https://sullygnome.com/<b>games</b>/30/<b>watched</b>",
"pagemap": {
"metatags": [
{
"viewport": "width=device-width, initial-scale=1.0"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lost Ark peaks at 1.27M on launch day, becomes most-watched ...",
"htmlTitle": "Lost Ark peaks at 1.27M on launch day, becomes <b>most</b>-<b>watched</b> ...",
"link": "https://www.invenglobal.com/articles/16393/lost-ark-peaks-at-127m-on-launch-day-becomes-most-watched-game-on-twitch",
"displayLink": "www.invenglobal.com",
"snippet": "Feb 9, 2022 ... Lost Ark: Source: Smilegate Lost Ark is now the most watched game on Twitch. According to analytics website TwitchTracker.",
"htmlSnippet": "Feb 9, 2022 <b>...</b> Lost Ark: Source: Smilegate Lost Ark is now the <b>most watched game on Twitch</b>. According to analytics website <b>TwitchTracker</b>.",
"cacheId": "3xZkfSBOVfQJ",
"formattedUrl": "https://www.invenglobal.com/.../lost-ark-peaks-at-127m-on-launch-day- becomes-most-watched-game-on-twitch",
"htmlFormattedUrl": "https://www.invenglobal.com/.../lost-ark-peaks-at-127m-on-launch-day- becomes-<b>most</b>-<b>watched</b>-<b>game-on-twitch</b>",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcQYWr9o2ASOTxPBGCmZTOjThHjjHbmeLppGDy8dA0g8yeXkxafIm6d8X78",
"width": "300",
"height": "168"
}
],
"metatags": [
{
"og:image": "https://static.invenglobal.com/upload/image/2022/02/09/r1644433519704165.jpeg",
"twitter:card": "summary_large_image",
"twitter:title": "Lost Ark peaks at 1.27M on launch day, becomes most-watched game on Twitch",
"og:type": "article",
"article:published_time": "2022-02-09T18:34:05+00:00",
"article:section": "Lost Ark",
"og:site_name": "InvenGlobal",
"twitter:url": "https://www.invenglobal.com/articles/16393/lost-ark-peaks-at-127m-on-launch-day-becomes-most-watched-game-on-twitch",
"og:title": "Lost Ark peaks at 1.27M on launch day, becomes most-watched game on Twitch",
"og:date": "Feb 9, 2022",
"_token": "BoHbtAOpx952okGPGYiMFoE508xwElpOsmruvfEB",
"title": "Lost Ark peaks at 1.27M on launch day, becomes most-watched game on Twitch - Inven Global",
"twitter:creator": "@InvenGlobal",
"og:description": "Source: Smilegate Lost Ark is now the most watched game on Twitch. According to analytics website TwitchTracker.",
"twitter:image": "https://static.invenglobal.com/upload/thumb/2022/02/09/w/b1644433519704165.jpg",
"article:tag": "Asmongold",
"fb:app_id": "173953323052298",
"twitter:site": "@InvenGlobal",
"article:modified_time": "2022-02-09T19:12:23+00:00",
"viewport": "width=device-width, initial-scale=1.0",
"twitter:description": "Source: Smilegate Lost Ark is now the most watched game on Twitch. According to analytics website TwitchTracker.",
"og:author": "John Popko",
"og:url": "https://www.invenglobal.com/articles/16393/lost-ark-peaks-at-127m-on-launch-day-becomes-most-watched-game-on-twitch"
}
],
"cse_image": [
{
"src": "https://static.invenglobal.com/upload/image/2022/02/09/r1644433519704165.jpeg"
}
],
"hatomfeed": [
{}
]
}
},
{
"kind": "customsearch#result",
"title": "Twitch Revenue and Usage Statistics (2022) - Business of Apps",
"htmlTitle": "<b>Twitch</b> Revenue and Usage Statistics (2022) - Business of Apps",
"link": "https://www.businessofapps.com/data/twitch-statistics/",
"displayLink": "www.businessofapps.com",
"snippet": "Sep 6, 2022 ... Most viewed games on Twitch. League of Legends is by far the most popular game on Twitch, with Riot Games e-sports tournaments streamed on the ...",
"htmlSnippet": "Sep 6, 2022 <b>...</b> <b>Most viewed games on Twitch</b>. League of Legends is by far the <b>most popular game on Twitch</b>, with Riot Games e-sports tournaments streamed on the&nbsp;...",
"cacheId": "O0iecpsbRoIJ",
"formattedUrl": "https://www.businessofapps.com/data/twitch-statistics/",
"htmlFormattedUrl": "https://www.businessofapps.com/data/<b>twitch</b>-statistics/",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcRtN_0Lka-RbCSDVQ2YWOV_KHCGyQIu0NvKCX0aCLKKpqUlcOox5yFNME_2",
"width": "278",
"height": "181"
}
],
"metatags": [
{
"og:image": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/uploads/2019/02/twitch.png",
"msapplication-square70x70logo": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/themes/boa/lib/assets/images/mstile-70x70.png",
"article:published_time": "2019-02-15T15:47:57+00:00",
"twitter:card": "summary_large_image",
"og:image:width": "292",
"og:site_name": "Business of Apps",
"twitter:label1": "Est. reading time",
"msapplication-wide310x150logo": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/themes/boa/lib/assets/images/mstile-310x150.png",
"og:image:type": "image/png",
"msapplication-tileimage": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/themes/boa/lib/assets/images/mstile-144x144.png",
"og:description": "Twitch is a livestreaming platform focused on video games. It was founded by Justin Kan in 2011, originally as a spin-off of Justin.tv. The latter started life in 2007 as a single channel, broadcasting Kans life live around the clock, pioneering the concept of lifecasting. The website attracted interest from others who were more interested in broadcasting their own lives than viewing that of Kans, which had nonetheless served as great exposure for Justin.TV. Acting on this interest, the site relaunched later in 2007, allowing users to create their own channels and broadcast their own content through the platform. Streaming games was not the original idea, but after seeing the interest from many users wanting to stream video games, the gaming category of Justin.TV was",
"twitter:image": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/uploads/2019/02/twitch.png",
"article:publisher": "https://www.facebook.com/thebusinessofapps",
"twitter:data1": "4 minutes",
"article_author": "Mansoor Iqbal",
"twitter:site": "@businessofapps",
"article:modified_time": "2022-09-06T13:15:15+01:00",
"msapplication-square310x310logo": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/themes/boa/lib/assets/images/mstile-310x310.png",
"msapplication-tilecolor": "#FFFFFF",
"og:type": "article",
"twitter:title": "Twitch Revenue and Usage Statistics (2022)",
"og:title": "Twitch Revenue and Usage Statistics (2022)",
"article_publisher": "Mansoor Iqbal",
"og:image:height": "190",
"og:updated_time": "2022-09-06T13:15:15+01:00",
"msapplication-square150x150logo": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/themes/boa/lib/assets/images/mstile-150x150.png",
"fb:app_id": "529576650555031",
"viewport": "width=device-width, initial-scale=1",
"twitter:description": "Twitch is a livestreaming platform focused on video games. It was founded by Justin Kan in 2011, originally as a spin-off of Justin.tv. The latter started life in 2007 as a single channel, broadcasting Kans life live around the clock, pioneering the concept of lifecasting. The website attracted interest from others who were more interested in broadcasting their own lives than viewing that of Kans, which had nonetheless served as great exposure for Justin.TV. Acting on this interest, the site relaunched later in 2007, allowing users to create their own channels and broadcast their own content through the platform. Streaming games was not the original idea, but after seeing the interest from many users wanting to stream video games, the gaming category of Justin.TV was",
"og:locale": "en_US",
"og:url": "https://www.businessofapps.com/data/twitch-statistics/"
}
],
"cse_image": [
{
"src": "https://1z1euk35x7oy36s8we4dr6lo-wpengine.netdna-ssl.com/wp-content/uploads/2019/02/twitch.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Grand Theft Auto V in Top 5 Most Watched Games on Twitch in ...",
"htmlTitle": "Grand Theft Auto V in Top 5 <b>Most Watched Games on Twitch</b> in ...",
"link": "https://www.whatgadget.net/grand-theft-auto-v-in-top-5-most-watched-games-on-twitch-in-august-83-3m-hours-in-viewership-more-than-double-dota-2/",
"displayLink": "www.whatgadget.net",
"snippet": "Sep 20, 2020 ... Grand Theft Auto V (GTA V) was the fifth most-watched game on Twitch in August 2020. It had 112140 concurrent viewers on average.",
"htmlSnippet": "Sep 20, 2020 <b>...</b> Grand Theft Auto V (GTA V) was the fifth <b>most</b>-<b>watched game on Twitch</b> in August 2020. It had 112140 concurrent viewers on average.",
"cacheId": "CXPa3QP1wMsJ",
"formattedUrl": "https://www.whatgadget.net/grand-theft-auto-v-in-top-5-most-watched-games -on-twitch-in-august-83-3m-hours-in-viewership-more-than-double-dota-2/",
"htmlFormattedUrl": "https://www.whatgadget.net/grand-theft-auto-v-in-<b>top</b>-5-<b>most</b>-<b>watched</b>-<b>games -on-twitch</b>-in-august-83-3m-hours-in-viewership-<b>more</b>-than-double-dota-2/",
"pagemap": {
"hcard": [
{
"url_text": "What Gadget",
"fn": "What Gadget",
"photo": "https://cdn.whatgadget.net/wp-content/uploads/2020/09/05082235/WG-copy-440x440.png",
"url": "https://www.whatgadget.net/author/what-gadget/"
}
],
"cse_thumbnail": [
{
"src": "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcRuEymPx8cHe4O2txUXe18ka_XzAv6dbWN6WI9W6A4zutMDBKYw9CuKbmOc",
"width": "329",
"height": "153"
}
],
"metatags": [
{
"og:image": "https://www.whatgadget.net/wp-content/uploads/2020/09/header.jpg",
"og:type": "article",
"article:published_time": "2020-09-20T13:22:35+00:00",
"og:image:width": "460",
"twitter:card": "summary_large_image",
"og:site_name": "What Gadget",
"og:title": "Grand Theft Auto V in Top 5 Most Watched Games on Twitch in August; 83.3M Hours in Viewership, More than Double Dota 2 - What Gadget",
"og:image:height": "215",
"twitter:label1": "Written by",
"twitter:label2": "Estimated reading time",
"og:image:type": "image/jpeg",
"msapplication-tileimage": "https://cdn.whatgadget.net/wp-content/uploads/2020/09/05082311/cropped-WG-copy-e1599134950257-270x270.png",
"og:description": "Grand Theft Auto V (GTA V) was the fifth most-watched game on Twitch in August 2020. It had 112,140 concurrent viewers on average.",
"twitter:creator": "@WhatGadgetUK",
"article:publisher": "https://www.facebook.com/WhatGadget.net",
"twitter:data1": "What Gadget",
"twitter:data2": "1 minute",
"twitter:site": "@WhatGadgetUK",
"article:modified_time": "2020-09-22T17:12:06+00:00",
"viewport": "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1",
"og:locale": "en_GB",
"og:url": "https://www.whatgadget.net/grand-theft-auto-v-in-top-5-most-watched-games-on-twitch-in-august-83-3m-hours-in-viewership-more-than-double-dota-2/"
}
],
"cse_image": [
{
"src": "https://www.whatgadget.net/wp-content/uploads/2020/09/header.jpg"
}
],
"hatomfeed": [
{}
]
}
}
]
}

View file

@ -17,6 +17,10 @@
"@bbob/core": "^2.8.1", "@bbob/core": "^2.8.1",
"@bbob/html": "^2.8.1", "@bbob/html": "^2.8.1",
"@bbob/preset-html5": "^2.8.1", "@bbob/preset-html5": "^2.8.1",
"@milkdown/core": "^6.4.1",
"@milkdown/preset-commonmark": "^6.4.2",
"@milkdown/prose": "^6.4.1",
"@milkdown/theme-nord": "^6.4.1",
"@vue-stripe/vue-stripe": "^4.4.2", "@vue-stripe/vue-stripe": "^4.4.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
@ -28,6 +32,7 @@
"firebaseui": "^4.8.0", "firebaseui": "^4.8.0",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"lodash.groupby": "^4.6.0", "lodash.groupby": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.orderby": "^4.6.0", "lodash.orderby": "^4.6.0",
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"marked": "^4.0.14", "marked": "^4.0.14",

View file

@ -1,3 +1,9 @@
<!-- TODO: translate strings -->
<!-- TODO: allow for anonymous boards, prompt to sign up -->
<!-- TODO: switch toggle -->
<!-- TODO: add markdown wysiwyg -->
<!-- TODO: add help section -->
<!-- TODO: bring notifications back! -->
<!-- TODO: fix favicon broken link --> <!-- TODO: fix favicon broken link -->
<template> <template>
<div <div
@ -103,7 +109,6 @@ export default {
}, },
updateWallpaperUrl(value) { updateWallpaperUrl(value) {
console.log(value);
this.backgroundImageUrl = value; this.backgroundImageUrl = value;
}, },
@ -116,7 +121,7 @@ export default {
}, },
init() { init() {
this.$store.dispatch('LOAD_IGDB_PLATFORMS'); // TODO: get platforms from constants
if (this.isPublicRoute) { if (this.isPublicRoute) {
return; return;

View file

@ -3,9 +3,7 @@
<!-- TODO: clone/fork board --> <!-- TODO: clone/fork board -->
<!-- TODO: like/favorite board --> <!-- TODO: like/favorite board -->
<template lang="html"> <template lang="html">
<div <div>
:class="['board px-3 pb-3', { dragging, empty }]"
>
<board-placeholder v-if="loading" /> <board-placeholder v-if="loading" />
<template v-else-if="showBoard"> <template v-else-if="showBoard">
@ -37,17 +35,8 @@
</b-button> </b-button>
</portal> </portal>
<game-list <basic-board v-if="board.type === 'basic'" />
v-for="(list, listIndex) in board.lists" <kanban-board v-else />
:list="list"
:listIndex="listIndex"
:key="list.name"
/>
<add-list
v-if="isBoardOwner"
:empty="empty"
/>
</template> </template>
<b-alert <b-alert
@ -62,16 +51,16 @@
<script> <script>
import BoardPlaceholder from '@/components/Board/BoardPlaceholder'; import BoardPlaceholder from '@/components/Board/BoardPlaceholder';
import AddList from '@/components/Board/AddList'; import KanbanBoard from '@/components/Board/KanbanBoard';
import GameList from '@/components/Lists/GameList'; import BasicBoard from '@/components/Board/BasicBoard';
import chunk from 'lodash.chunk'; import chunk from 'lodash.chunk';
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
export default { export default {
components: { components: {
GameList,
BoardPlaceholder, BoardPlaceholder,
AddList, KanbanBoard,
BasicBoard,
}, },
data() { data() {
@ -99,10 +88,6 @@ export default {
return this.$route.params?.id; return this.$route.params?.id;
}, },
empty() {
return this.board?.lists?.length === 0;
},
isBoardCached() { isBoardCached() {
return this.board.id === this.boardId; return this.board.id === this.boardId;
}, },

View file

@ -26,27 +26,9 @@
/> />
</b-form-group> </b-form-group>
<!-- <b-form-group <pre>
label="Board template" {{ board.type }}
> </pre>
<b-form-radio-group
v-model="selectedTemplate"
:options="boardTemplatesOptions"
name="radios-btn-default"
description="Optional"
/>
<b-row v-if="selectedTemplate" class="mt-3">
<b-col v-for="column in boardTemplates[selectedTemplate]" :key="column">
<b-card
:header="column"
header-tag="header"
header-class="p-1 pl-2"
hide-footer
/>
</b-col>
</b-row>
</b-form-group> -->
<b-button <b-button
variant="primary" variant="primary"
@ -69,20 +51,10 @@ export default {
name: '', name: '',
description: '', description: '',
lists: [], lists: [],
type: 'kanban',
}, },
saving: false, saving: false,
selectedTemplate: null, selectedTemplate: null,
// boardTemplatesOptions: [
// { value: null, text: 'Blank' },
// { value: 'standard', text: 'Standard' },
// { value: 'detailed', text: 'Detailed' },
// { value: 'completionist', text: 'Completionist' },
// ],
// boardTemplates: {
// standard: ['Owned', 'Wishlist'],
// detailed: ['Physical', 'Digital', 'Wishlist'],
// completionist: ['Owned', 'Playing', 'Completed'],
// },
}; };
}, },
@ -94,16 +66,17 @@ export default {
// TODO: put default board in constant // TODO: put default board in constant
const payload = { const payload = {
...this.board, ...this.board,
// TODO: set default lists based on board type
games: [],
lastUpdated: Date.now(),
lists: [{ lists: [{
name: 'Click to rename', name: 'Click to rename',
games: [358], games: [],
settings: { settings: {
showReleaseDates: false, showReleaseDates: false,
sortOrder: 'sortByCustom', sortOrder: 'sortByCustom',
showGameTags: false, showGameTags: false,
showGameNotes: false, showGameNotes: false,
showGameProgress: false,
highlightCompletedGames: false,
showGameCount: false, showGameCount: false,
view: 'single' view: 'single'
}, },

View file

@ -1,3 +1,4 @@
<!-- TODO: refactor saving, create payload locally -->
<template lang="html"> <template lang="html">
<section> <section>
<b-container> <b-container>
@ -44,6 +45,17 @@
/> />
</b-form-group> </b-form-group>
<b-form-group
label="Board type"
label-for="name"
>
<b-dropdown id="dropdown-1" :text="board.type">
<b-dropdown-item @click="setBoardType('kanban')">Kanban</b-dropdown-item>
<!-- <b-dropdown-item @click="setBoardType('tiers')">Tiers</b-dropdown-item> -->
<b-dropdown-item @click="setBoardType('basic')">Basic</b-dropdown-item>
</b-dropdown>
</b-form-group>
<b-form-group <b-form-group
:label="$t('board.settings.descriptionLabel')" :label="$t('board.settings.descriptionLabel')"
label-for="description" label-for="description"
@ -145,7 +157,6 @@ import { mapState, mapGetters } from 'vuex';
import WallpapersList from '@/components/WallpapersList'; import WallpapersList from '@/components/WallpapersList';
import VSwatches from 'vue-swatches' import VSwatches from 'vue-swatches'
import MiniBoard from '@/components/Board/MiniBoard'; import MiniBoard from '@/components/Board/MiniBoard';
import orderby from 'lodash.orderby';
export default { export default {
components: { components: {
@ -180,10 +191,17 @@ export default {
this.board = await this.$store.dispatch('LOAD_BOARD', this.boardId); this.board = await this.$store.dispatch('LOAD_BOARD', this.boardId);
if (!this.board.type) this.board.type = 'kanban';
this.loading = false; this.loading = false;
}, },
setBoardType(type) {
console.log('type', type);
// TODO: check if switching to basic while having more than 1 list
this.board.type = type;
},
confirmDelete() { confirmDelete() {
this.$bvModal.msgBoxConfirm('Are you sure you want to delete this board?', { this.$bvModal.msgBoxConfirm('Are you sure you want to delete this board?', {
title: 'Delete board', title: 'Delete board',
@ -208,7 +226,7 @@ export default {
this.loading = false; this.loading = false;
this.$bvToast.toast('Board removed'); this.$bvToast.toast('Board removed');
this.$router.push({ name: 'home' }); this.$router.push({ name: 'boards' });
}, },
selectWallpaper(wallpaper) { selectWallpaper(wallpaper) {
@ -219,17 +237,7 @@ export default {
async saveBoard() { async saveBoard() {
this.saving = true; this.saving = true;
// const { board } = this; this.board.lastUpdated = Date.now();
//
// const payload = {
// ...board,
// description: this.description,
// name: this.name,
// isPublic: this.isPublic,
// theme: this.theme,
// };
// this.$store.commit('SET_ACTIVE_BOARD', this.board);
await this.$store.dispatch('SAVE_BOARD') await this.$store.dispatch('SAVE_BOARD')
.catch(() => { .catch(() => {

View file

@ -120,14 +120,14 @@
<!-- TODO: add release date styles: countdown/simple date --> <!-- TODO: add release date styles: countdown/simple date -->
<b-form-checkbox <!-- <b-form-checkbox
v-model="list.settings.showGameProgress" v-model="list.settings.showGameProgress"
name="check-button" name="check-button"
class="mb-2" class="mb-2"
switch switch
> >
{{ $t('board.list.showGameProgress') }} {{ $t('board.list.showGameProgress') }}
</b-form-checkbox> </b-form-checkbox> -->
<b-form-checkbox <b-form-checkbox
v-model="list.settings.highlightCompletedGames" v-model="list.settings.highlightCompletedGames"

View file

@ -2,7 +2,6 @@
<b-dropdown <b-dropdown
no-caret no-caret
toggle-class="p-0 px-2 mt-n2" toggle-class="p-0 px-2 mt-n2"
size="sm"
> >
<template #button-content> <template #button-content>
<i class="fa fa-plus small pr-1" aria-hidden="true" /> <i class="fa fa-plus small pr-1" aria-hidden="true" />
@ -66,7 +65,6 @@ export default {
computed: { computed: {
...mapState(['games', 'boards', 'wallpapers']), ...mapState(['games', 'boards', 'wallpapers']),
// TODO: handle this at action/mutation level OR use getter at least
filteredBoards() { filteredBoards() {
return this.boards return this.boards
.filter(({ name }) => name.toLowerCase().includes(this.searchText.toLowerCase())); .filter(({ name }) => name.toLowerCase().includes(this.searchText.toLowerCase()));

View file

@ -0,0 +1,34 @@
<template lang="html">
<div class="basic-board my-3">
<basic-game-list :list="list" />
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import BasicGameList from '@/components/Lists/BasicGameList';
export default {
components: {
BasicGameList,
},
computed: {
...mapState(['board']),
list() {
const [firstList] = this.board?.lists;
return firstList || [];
}
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.basic-board {
width: 600px;
max-width: 100%;
margin: 0 auto;
}
</style>

View file

@ -0,0 +1,40 @@
<template lang="html">
<div class="board px-3 pb-3">
<game-list
v-for="(list, listIndex) in board.lists"
:list="list"
:listIndex="listIndex"
:key="list.name"
/>
<add-list
v-if="isBoardOwner"
:empty="empty"
/>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import AddList from '@/components/Board/AddList';
import GameList from '@/components/Lists/GameList';
export default {
components: {
GameList,
AddList,
},
computed: {
...mapState(['board']),
...mapGetters(['isBoardOwner']),
empty() {
return this.board?.lists?.length === 0;
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -1,10 +1,13 @@
<!-- TODO: make steam description match standard desc -->
<!-- TODO: http://localhost:4000/g/114146/angry-video-game-nerd-i-and-ii-deluxe -->
<template lang="html"> <template lang="html">
<div class="game-description"> <div :class="['game-description', source]">
<b-spinner v-if="loading" class="spinner-centered" /> <b-spinner v-if="loading" class="spinner-centered" />
<template v-else> <template v-else>
<div v-html="description" /> <div v-html="description" />
<span class="text-muted mt-n3 mb-3">Source: {{ source }}</span> <!-- TODO: link to source -->
<span class="text-muted mt-n3 mb-3 text-capitalize">Source: {{ source }}</span>
</template> </template>
</div> </div>
</template> </template>
@ -29,8 +32,8 @@ export default {
}, },
source() { source() {
if (this.wikipediaExtract) return 'Wikipedia'; if (this.wikipediaExtract) return 'wikipedia';
if (this.steamDescription) return 'Steam'; if (this.steamDescription) return 'steam';
return 'IGDB'; return 'IGDB';
}, },
@ -78,6 +81,10 @@ export default {
<style lang="scss" rel="stylesheet/scss"> <style lang="scss" rel="stylesheet/scss">
.game-description { .game-description {
&.steam {
// Steam overrides
}
h2, h3 { h2, h3 {
margin: 0; margin: 0;
} }

View file

@ -1,8 +1,8 @@
<template lang="html"> d<template lang="html">
<b-card class="mt-3 small"> <b-card class="mt-3 small">
<!-- TODO: merge release Dates and platofmrs -->
<div v-if="gameGenres" class="pr-2 pb-2"> <div v-if="gameGenres" class="pr-2 pb-2">
<strong>Genres:</strong> <strong class="text-muted">Genres:</strong>
{{ gameGenres }} {{ gameGenres }}
</div> </div>
@ -30,11 +30,18 @@
<div class="pr-2 pb-2"> <div class="pr-2 pb-2">
<strong class="text-muted">Available for: </strong> <strong class="text-muted">Available for: </strong>
<span class="text-wrap">{{ gamePlatforms || 'N/A' }}</span> <span
v-for="(platform, index) in gamePlatforms"
:key="platform.id"
>
<b-link :to="{ name: 'platform', params: { id: platform.id }}">{{ platform.name }}</b-link>
<template v-if="index < gamePlatforms.length - 1">, </template>
</span>
</div> </div>
<div class="pr-2 pb-2"> <!-- TODO: merge release Dates and platforms -->
<strong>{{ $t('board.gameModal.releaseDate') }}</strong> <!-- <div class="pr-2 pb-2">
<strong class="text-muted">{{ $t('board.gameModal.releaseDate') }}</strong>
<ol v-if="releaseDates" class="list-unstyled mb-0"> <ol v-if="releaseDates" class="list-unstyled mb-0">
<li <li
v-for="{ id, platform, date } in releaseDates" v-for="{ id, platform, date } in releaseDates"
@ -47,8 +54,33 @@
<div v-else> <div v-else>
Not released yet Not released yet
</div> </div>
</div> -->
<div class="pr-2 pb-2">
<strong class="text-muted">Tags</strong>
<br />
<b-button
v-for="({ bgColor, textColor, name, index }) in tagsApplied"
:key="index"
rounded
size="sm"
variant="transparent"
class="mr-1 mb-2"
:style="`background-color: ${bgColor}; color: ${textColor}`"
:to="{ name: 'tag.edit', params: { id: index } }"
>
<i class="fa-solid fa-tag mr-1" />
{{ name }}
</b-button>
<game-tags-dropdown v-if="user" />
</div> </div>
<strong class="text-muted">Other links</strong>
<br>
<b-button <b-button
v-for="{ url, id, icon, svg } in gameLinks" v-for="{ url, id, icon, svg } in gameLinks"
:href="url" :href="url"
@ -77,14 +109,34 @@
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import GameTagsDropdown from '@/components/Game/GameTagsDropdown';
export default { export default {
components: {
GameTagsDropdown,
},
data() {
return {
progress: 0,
saving: false,
deleting: false,
}
},
computed: { computed: {
...mapGetters(['platformNames', 'gameLinks']), ...mapGetters(['platformNames', 'gameLinks']),
...mapState(['game']), ...mapState(['game', 'tags', 'user', 'progresses']),
tagsApplied() {
if (!this.tags) return [];
return this.tags?.map((tag, index) => ({ ...tag, index }))
.filter((tag) => tag?.games?.includes(this.game?.id));
},
gamePlatforms() { gamePlatforms() {
return this.game?.platforms?.map(({ name }) => name).join(', '); return this.game?.platforms;
}, },
gameDevelopers() { gameDevelopers() {
@ -135,5 +187,50 @@ export default {
}); });
}, },
}, },
async mounted() {
if (!this.tags) {
await this.$store.dispatch('LOAD_TAGS');
}
this.progress = this.progresses?.[this.game?.id]
? JSON.parse(JSON.stringify(this.progresses?.[this.game?.id]))
: 0;
},
methods: {
async deleteProgress() {
const { id, name } = this.game;
this.deleting = true;
this.$store.commit('REMOVE_GAME_PROGRESS', id);
await this.$store.dispatch('SAVE_PROGRESSES_NO_MERGE')
.catch(() => {
this.$bvToast.toast('There was an error deleting your progress', { title: `${name} progress`, variant: 'error' });
this.deleting = false;
});
this.deleting = false;
},
async saveProgress() {
this.saving = true;
this.$store.commit('SET_GAME_PROGRESS', {
progress: this.progress,
gameId: this.game?.id,
});
await this.$store.dispatch('SAVE_PROGRESSES')
.catch(() => {
this.saving = false;
this.$bvToast.toast('There was an error saving your progress', { variant: 'error' });
});
this.saving = false;
},
},
}; };
</script> </script>

View file

@ -1,13 +1,10 @@
<template lang="html"> <template lang="html">
<div v-if="boardsWithGame.length" class="mt-4"> <div class="mt-4">
<p class="small mb-2">Found in</p>
<b-button <b-button
v-for="board in boardsWithGame" v-for="board in boardsWithGame"
:to="{ name: 'board', params: { id: board.id } }"
:key="board.id" :key="board.id"
variant="light" :to="{ name: 'board', params: { id: board.id } }"
size="sm" variant="outline-primary"
class="mr-2 py-0 mb-2" class="mr-2 py-0 mb-2"
> >
<small>{{ board.name }}</small> <small>{{ board.name }}</small>
@ -30,8 +27,9 @@ export default {
...mapState(['board', 'game', 'boards']), ...mapState(['board', 'game', 'boards']),
boardsWithGame() { boardsWithGame() {
return this.boards const filteredBoards = this.boards?.filter(({ lists }) => lists.some(({ games }) => games.includes(this.game.id))) || [];
?.filter(({ lists }) => lists.some(({ games }) => games.includes(this.game.id))) || [];
return filteredBoards;
}, },
}, },
}; };

View file

@ -1,33 +1,45 @@
<template lang="html"> <template lang="html">
<div class="mt-3 d-flex flex-wrap"> <div class="mt-3">
<div <b-row no-gutters>
v-for="({ imageUrl, isVideo, isCover }, index) in gameMedia" <b-col
v-show="index > 0" v-for="({ imageUrl, isVideo, isCover }, index) in previewThumbs"
:key="index" :key="index"
class="mr-2 align-items-center text-center mb-2 rounded cursor-pointer position-relative" cols="3"
> >
<i <div
v-if="isVideo" class="mr-2 align-items-center text-center mb-2 rounded cursor-pointer position-relative"
class="fa-solid fa-play video-indicator position-absolute text-white" >
/> <i
v-if="isVideo"
class="fa-solid fa-play video-indicator position-absolute text-white"
/>
<div v-if="isCover" class="position-absolute cover-indicator text-light small w-100 bg-dark rounded-bottom"> <div v-if="isCover" class="position-absolute cover-indicator text-light small w-100 bg-dark rounded-bottom">
Cover Cover
</div> </div>
<b-img <b-img
:src="imageUrl" :src="imageUrl"
rounded rounded
height="80" fluid
@click="viewMedia(index)" @click="viewMedia(index)"
/> />
</div> </div>
</b-col>
<b-button
v-if="totalMedia > 3"
@click="viewMedia(3)"
>
<i class="fa-solid fa-photo-film" />
{{ totalMedia - 3 }} more
</b-button>
</b-row>
<b-modal <b-modal
id="mediaModal" id="mediaModal"
centered centered
hide-footer hide-footer
size="xl"
:visible="visible" :visible="visible"
@show="open" @show="open"
@hidden="close" @hidden="close"
@ -56,7 +68,7 @@
<div v-if="selectedMedia && gameMedia.length" class="game-media"> <div v-if="selectedMedia && gameMedia.length" class="game-media">
<b-embed <b-embed
v-if="selectedMedia && selectedMedia.isVideo" v-if="isSelectedMediaVideo"
type="iframe" type="iframe"
aspect="16by9" aspect="16by9"
:src="selectedMedia.videoUrl" :src="selectedMedia.videoUrl"
@ -71,7 +83,6 @@
/> />
<footer class="mt-2 d-flex overflow-auto pb-2"> <footer class="mt-2 d-flex overflow-auto pb-2">
<pre class="bg-success">{{ activeIndex }}</pre>
<b-img <b-img
v-for="(media, index) in gameMedia" v-for="(media, index) in gameMedia"
:key="media.imageUrl" :key="media.imageUrl"
@ -97,7 +108,7 @@ export default {
data() { data() {
return { return {
activeIndex: null, activeIndex: null,
maxThumbnails: 3, maxThumbnails: 4,
saving: false, saving: false,
}; };
}, },
@ -106,10 +117,24 @@ export default {
...mapGetters(['isBoardOwner', 'gameMedia']), ...mapGetters(['isBoardOwner', 'gameMedia']),
...mapState(['board', 'game']), ...mapState(['board', 'game']),
previewThumbs() {
const previewThumbs = this.gameMedia.slice(0, this.maxThumbnails);
return previewThumbs;
},
isSelectedMediaVideo() {
return this.selectedMedia?.isVideo;
},
selectedMedia() { selectedMedia() {
return this.gameMedia?.[this.activeIndex]; return this.gameMedia?.[this.activeIndex];
}, },
totalMedia() {
return this.gameMedia?.length || 0;
},
visible() { visible() {
return this.activeIndex !== null; return this.activeIndex !== null;
}, },
@ -158,7 +183,7 @@ export default {
.game-media { .game-media {
display: grid; display: grid;
grid-gap: 1rem; grid-gap: 1rem;
grid-template-rows: 50vh auto; grid-template-rows: 1fr auto;
} }
.selected-image { .selected-image {
@ -173,4 +198,9 @@ export default {
.cover-indicator { .cover-indicator {
bottom: 0; bottom: 0;
} }
.media-button {
padding: 21px 16px;
height: 100%;
}
</style> </style>

View file

@ -0,0 +1,40 @@
<template lang="html">
<div>
<strong class="text-muted">Completed: {{ progress }}</strong>
<b-form-input
size="lg"
v-model="progress"
type="range"
max="100"
step="1"
/>
<b-button
variant="primary"
:disabled="saving"
class="mr-2"
@click="saveProgress"
>
<b-spinner small v-if="saving" />
<span v-else>{{ $t('global.save') }}</span>
</b-button>
<b-button
:disabled="deleting"
variant="danger"
@click="deleteProgress"
>
<b-spinner small v-if="deleting" />
<i v-else class="fas fa-trash fa-fw" aria-hidden />
</b-button>
</div>
</template>
<script>
export default {
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -61,12 +61,12 @@ export default {
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" rel="stylesheet/scss" scoped>
.similar-games { .similar-games {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(10, 1fr);
grid-gap: 1rem; grid-gap: 1rem;
margin-bottom: 20vh; margin-bottom: 20vh;
@media(max-width: 780px) { @media(max-width: 780px) {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(5, 1fr);
} }
} }
</style> </style>

View file

@ -35,7 +35,7 @@
sm="6" sm="6"
md="4" md="4"
lg="3" lg="3"
class="px-3 pb-4" class="px-1 pb-2"
> >
<mini-board <mini-board
:board="board" :board="board"

View file

@ -0,0 +1,17 @@
<template lang="html">
<div>
game card here
type: <pre>{{ type }}</pre>
</div>
</template>
<script>
export default {
props: {
type: string,
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -0,0 +1,155 @@
<template lang="html">
<draggable
class="games centered"
handle=".card"
ghost-class="card-placeholder"
drag-class="border-selected"
chosen-class="border-primary"
filter=".drag-filter"
delay="50"
animation="500"
:list="list.games"
:move="validateMove"
:disabled="draggingDisabled"
:group="{ name: 'games' }"
@end="dragEnd"
@start="dragStart"
>
<b-card
no-body
class="mb-2 flex-row align-items-center cursor-pointer"
v-for="gameId in sortedGames"
:key="gameId"
@click="openGame(gameId, list)"
>
<b-img
:src="$options.getThumbnailUrl(games[gameId])"
alt="Image"
class="m-2"
rounded
fluid
width="160"
/>
<span class="d-flex w-100 justify-content-center mr-2">{{ games[gameId].name }}</span>
</b-card>
<div v-if="isEmpty && isBoardOwner">
<b-button
variant="light"
block
class="mb-2"
:disabled="!isBoardOwner"
:to="{ name: 'search', query: { boardId: board.id, listIndex: 0 } }"
>
<template v-if="isBoardOwner">Add games</template>
<template v-else>Empty list</template>
</b-button>
</div>
</draggable>
</template>
<script>
import draggable from 'vuedraggable';
import slugify from 'slugify'
import orderby from 'lodash.orderby';
import { DEFAULT_LIST_VIEW } from '@/constants';
import { mapState, mapGetters } from 'vuex';
import { getThumbnailUrl } from '@/utils';
export default {
getThumbnailUrl,
components: {
draggable,
},
props: {
list: {
type: Object,
default: () => {},
},
},
data() {
return {
draggingId: null,
editing: false,
};
},
computed: {
...mapState(['games', 'dragging', 'progresses', 'board', 'user', 'settings']),
...mapGetters(['isBoardOwner']),
draggingDisabled() {
return !this.user || !this.isBoardOwner;
},
autoSortEnabled() {
return ['sortByName', 'sortByRating', 'sortByReleaseDate', 'sortByProgress'].includes(this.list?.settings?.sortOrder);
},
sortedGames() {
const { settings, games } = this.list;
const sortOrder = settings.sortOrder || 'sortByCustom';
switch (sortOrder) {
case 'sortByCustom': return this.list.games;
case 'sortByProgress': return orderby(games, [game => this.progresses[game] || 0], ['desc']);
case 'sortByRating': return orderby(games, [game => this.games[game].rating || 0], ['desc']);
case 'sortByName': return orderby(games, [game => this.games[game].name]);
default:
return this.list.games;
}
},
isEmpty() {
return this.list.games.length === 0;
},
singleList() {
return this.board.lists.length === 1;
},
},
methods: {
openGame(id, list) {
const slug = slugify(this.games[id].slug, { lower: true });
this.$router.push({
name: 'game',
params: {
id,
slug,
boardId: this.board.id,
},
});
},
validateMove({ from, to }) {
const sameList = from.id === to.id;
const notInList = !this.board?.lists?.[to.id]?.games?.includes(Number(this.draggingId));
return sameList || notInList && !sameList;
},
dragStart({ item }) {
this.$store.commit('SET_DRAGGING_STATUS', true);
this.draggingId = item.id;
},
dragEnd() {
this.$store.commit('SET_DRAGGING_STATUS', false);
this.saveBoard();
},
async saveBoard() {
await this.$store.dispatch('SAVE_BOARD')
.catch(() => {
this.$store.commit('SET_SESSION_EXPIRED', true);
});
},
},
};
</script>

View file

@ -18,11 +18,11 @@
<div class="align-items-center d-flex ml-auto mr-2"> <div class="align-items-center d-flex ml-auto mr-2">
<portal-target name="headerActions" /> <portal-target name="headerActions" />
<!-- <b-button v-if="user" class="mr-2" variant="success" :to="{ name: 'upgrade' }"> <b-button :to="{ name: 'search' }" class="d-sm-none">
Upgrade <i class="fa fa-search" aria-hidden="true"></i>
</b-button> --> </b-button>
<search-box /> <search-box class="d-none d-sm-block" />
<b-button <b-button
v-if="!user" v-if="!user"

View file

@ -112,30 +112,6 @@ export const KEYBOARD_SHORTCUTS = {
ROUTE_settings: ['shift', 's'], ROUTE_settings: ['shift', 's'],
}; };
export const PLATFORM_FILTER_FIELDS = [
null,
'all',
'console',
// 'arcade',
// 'platform',
'operating_system',
'portable_console',
'computer',
];
export const PLATFORM_SORT_FILEDS = [
'generation',
'name',
];
export const PLATFORM_BG_HEX = {
167: '#222',
166: '#000',
48: '#2e6db4',
49: '#177d3e',
130: '#ce181e',
};
export const LIST_VIEWS = { export const LIST_VIEWS = {
single: 'Single', single: 'Single',
grid: 'Grid', grid: 'Grid',

View file

@ -15,14 +15,6 @@ const routes = [
title: 'Notes' title: 'Notes'
}, },
}, },
{
name: 'game.progress',
path: '/g/:id/:slug/progress',
component: () => import(/* webpackChunkName: "game" */ '@/game/pages/GameProgressPage'),
meta: {
title: 'Track game progress'
},
},
{ {
name: 'game', name: 'game',
path: '/g/:id/:slug', path: '/g/:id/:slug',

View file

@ -30,6 +30,8 @@
</portal> </portal>
<b-col cols="12" sm="6"> <b-col cols="12" sm="6">
<div ref="editor" />
<game-note <game-note
v-if="showPreview" v-if="showPreview"
:note="{ note }" :note="{ note }"

View file

@ -26,6 +26,7 @@
cols="12" cols="12"
md="4" md="4"
xl="3" xl="3"
class="text-center"
> >
<b-img <b-img
:src="gameCoverUrl" :src="gameCoverUrl"
@ -34,27 +35,12 @@
fluid fluid
/> />
<game-note <game-media />
v-if="note"
:note="note"
class="cursor-pointer mt-3 d-none d-md-block"
@click.native="$router.push({ name: 'game.notes', params: { id: game.id, slug: game.slug } })"
/>
<b-button
v-else
size="sm"
variant="warning"
:to="{ name: 'game.notes', params: { id: game.id, slug: game.slug } }"
class="mt-2"
>
Add note
</b-button>
<b-button <b-button
v-if="gameNews.length" v-if="gameNews.length"
size="sm" size="sm"
class="mt-2 ml-2" class="mt-2 ml-2 d-none d-md-block"
:to="{ name: 'game.news', params: { id: game.id, slug: game.slug } }" :to="{ name: 'game.news', params: { id: game.id, slug: game.slug } }"
> >
<b-badge>{{ gameNews.length }}</b-badge> <b-badge>{{ gameNews.length }}</b-badge>
@ -62,14 +48,8 @@
</b-button> </b-button>
<!-- <amazon-links class="mt-2" /> --> <!-- <amazon-links class="mt-2" /> -->
<game-in-list :class="{ 'text-white': hasWallpaper }" />
<!-- <game-speedruns /> --> <!-- <game-speedruns /> -->
<!-- <pre>{{ gameAchievements }}</pre> -->
<!-- <div v-if="gameAchievements"> -->
<!-- <pre>{{ gameAchievements }}</pre> -->
<!-- </div> -->
</b-col> </b-col>
<b-col <b-col
@ -80,46 +60,30 @@
<article :class="[' rounded', hasWallpaper ? 'bg-white mt-2 mt-md-0 p-3' : 'px-sm-3 p-0']"> <article :class="[' rounded', hasWallpaper ? 'bg-white mt-2 mt-md-0 p-3' : 'px-sm-3 p-0']">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<game-titles /> <game-titles />
<b-link
class="align-self-end ml-2 small"
:to="{ name: 'game.progress', params: { id: game.id, slug: game.slug } }"
>
<template v-if="progress > 0">
{{ progress }}% completed
</template>
<template v-else>
Set progress
</template>
</b-link>
</div> </div>
<template v-if="tagsApplied.length">
<b-button
v-for="({ bgColor, textColor, name, index }) in tagsApplied"
:key="name"
rounded
size="sm"
variant="transparent"
class="mr-1 mb-2"
:style="`background-color: ${bgColor}; color: ${textColor}`"
:to="{ name: 'tag.edit', params: { id: index } }"
>
<i class="fa-solid fa-tag mr-1" />
{{ name }}
</b-button>
</template>
<game-tags-dropdown v-if="user" />
<aside class="supplemental-info bg-white field float-right ml-5 pb-2"> <aside class="supplemental-info bg-white field float-right ml-5 pb-2">
<game-details /> <game-details />
<game-note
v-if="note"
:note="note"
class="cursor-pointer mt-3 d-none d-md-block"
@click.native="$router.push({ name: 'game.notes', params: { id: game.id, slug: game.slug } })"
/>
<b-button
v-else
size="sm"
variant="warning"
:to="{ name: 'game.notes', params: { id: game.id, slug: game.slug } }"
class="mt-2"
>
Add note
</b-button>
</aside> </aside>
<game-description /> <game-description />
<game-media-viewer /> <game-in-list :class="{ 'text-white': hasWallpaper }" />
<game-ratings /> <game-ratings />
</article> </article>
@ -160,8 +124,7 @@ import { mapState, mapGetters } from 'vuex';
import { WEBSITE_CATEGORIES } from '@/constants'; import { WEBSITE_CATEGORIES } from '@/constants';
// import AmazonLinks from '@/components/Game/AmazonLinks'; // import AmazonLinks from '@/components/Game/AmazonLinks';
import GameDetails from '@/components/Game/GameDetails'; import GameDetails from '@/components/Game/GameDetails';
import GameTagsDropdown from '@/components/Game/GameTagsDropdown'; import GameMedia from '@/components/Game/GameMedia';
import GameMediaViewer from '@/components/Game/GameMediaViewer';
import GameTitles from '@/components/Game/GameTitles'; import GameTitles from '@/components/Game/GameTitles';
import GameRatings from '@/components/Game/GameRatings'; import GameRatings from '@/components/Game/GameRatings';
import GameDescription from '@/components/Game/GameDescription'; import GameDescription from '@/components/Game/GameDescription';
@ -177,9 +140,8 @@ export default {
GameNote, GameNote,
GameDescription, GameDescription,
GameDetails, GameDetails,
GameTagsDropdown,
GameTitles, GameTitles,
GameMediaViewer, GameMedia,
GameRatings, GameRatings,
// GameSpeedruns, // GameSpeedruns,
SimilarGames, SimilarGames,
@ -219,6 +181,7 @@ export default {
originBoardId() { originBoardId() {
return this.$route?.params?.boardId; return this.$route?.params?.boardId;
return this.$route?.params?.boardId;
}, },
// gameAchievements() { // gameAchievements() {
@ -229,13 +192,6 @@ export default {
return this.notes[this.game?.id] || null; return this.notes[this.game?.id] || null;
}, },
tagsApplied() {
if (!this.tags) return [];
return this.tags?.map((tag, index) => ({ ...tag, index }))
.filter((tag) => tag?.games?.includes(this.game?.id));
},
legalNotice() { legalNotice() {
return this.game?.steam?.legal_notice; return this.game?.steam?.legal_notice;
}, },
@ -256,12 +212,6 @@ export default {
: '/no-image.jpg'; : '/no-image.jpg';
}, },
progress() {
const { gameId, progresses } = this;
return progresses[gameId] || null;
},
gameId() { gameId() {
return this.$route.params.id; return this.$route.params.id;
}, },
@ -288,9 +238,6 @@ export default {
async mounted() { async mounted() {
if (!this.twitchToken) return this.waitAndLoadGame(); if (!this.twitchToken) return this.waitAndLoadGame();
if (!this.tags) {
await this.$store.dispatch('LOAD_TAGS');
}
this.loadGame(); this.loadGame();
}, },
@ -337,7 +284,7 @@ export default {
// TODO: find more precise way to load GOG game, based on id? // TODO: find more precise way to load GOG game, based on id?
const gogPage = this.game?.websites?.find(({ category }) => category !== GOG_CATEGORY_ID); const gogPage = this.game?.websites?.find(({ category }) => category !== GOG_CATEGORY_ID);
if (gogPage) await this.$store.dispatch('LOAD_GOG_GAME', this.game.name).catch((e) => {}); if (gogPage) await this.$store.dispatch('LOAD_GOG_GAME', this.game?.name).catch((e) => {});
// const wikipediaData = this.game?.websites?.find(({ url, category }) => url && category === WEBSITE_CATEGORIES.WIKIPEDIA); // const wikipediaData = this.game?.websites?.find(({ url, category }) => url && category === WEBSITE_CATEGORIES.WIKIPEDIA);
const wikipediaSlug = this.game?.websites const wikipediaSlug = this.game?.websites

View file

@ -1,126 +0,0 @@
<!-- TODO: switch to modal or dropdown -->
<template lang="html">
<section>
<b-container>
<portal to="pageTitle">
<div>
<b-button
:to="{ name: 'game', params: { id: game.id, slug: game.slug } }"
variant="light"
class="mr-2"
>
<i class="fa-solid fa-chevron-left" />
</b-button>
Track progress
</div>
</portal>
<b-row>
<b-col cols="12" sm="6">
<router-link :to="{ name: 'game', params: { id: game.id, slug: game.slug }}" class="float-right">
<b-img :src="gameCoverUrl" fluid rounded />
</router-link>
</b-col>
<b-col cols="12" sm="6" class="mt-3 mt-sm-0 mb-3">
<b-input-group :prepend="`${progress}%`" class="field mb-2">
<b-form-input
size="lg"
v-model="progress"
type="range"
max="100"
step="1"
/>
</b-input-group>
<b-button
variant="primary"
:disabled="saving"
class="mr-2"
@click="saveProgress"
>
<b-spinner small v-if="saving" />
<span v-else>{{ $t('global.save') }}</span>
</b-button>
<b-button
:disabled="deleting"
variant="danger"
@click="deleteProgress"
>
<b-spinner small v-if="deleting" />
<i v-else class="fas fa-trash fa-fw" aria-hidden />
</b-button>
</b-col>
</b-row>
</b-container>
</section>
</template>
<script>
import { mapState } from 'vuex';
import { getGameCoverUrl } from '@/utils';
export default {
data() {
return {
progress: 0,
saving: false,
deleting: false,
};
},
computed: {
...mapState(['progresses', 'game']),
gameCoverUrl() {
return getGameCoverUrl(this.game);
},
},
mounted() {
this.progress = this.progresses?.[this.game?.id]
? JSON.parse(JSON.stringify(this.progresses?.[this.game?.id]))
: 0;
},
methods: {
async deleteProgress() {
const { id, name } = this.game;
this.deleting = true;
this.$store.commit('REMOVE_GAME_PROGRESS', id);
await this.$store.dispatch('SAVE_PROGRESSES_NO_MERGE')
.catch(() => {
this.$bvToast.toast('There was an error deleting your progress', { title: `${name} progress`, variant: 'error' });
this.deleting = false;
});
this.deleting = false;
this.$router.push({ name: 'game', params: { id: this.game.id, slug: this.game.slug }});
},
async saveProgress() {
this.saving = true;
this.$store.commit('SET_GAME_PROGRESS', {
progress: this.progress,
gameId: this.game?.id,
});
await this.$store.dispatch('SAVE_PROGRESSES')
.catch(() => {
this.saving = false;
this.$bvToast.toast('There was an error saving your progress', { variant: 'error' });
});
this.saving = false;
this.$router.push({ name: 'game', params: { id: this.game.id, slug: this.game.slug }});
},
},
};
</script>

View file

@ -1,3 +1,5 @@
// TODO: make one game card component, use props for type
// TODO: disolve
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
export default { export default {

View file

@ -1,22 +1,113 @@
<!-- TODO: only load platforms once -->
<!-- TODO: add infinite loader / pagination -->
<!-- TODO: add sorting -->
<template lang="html"> <template lang="html">
<div> <section>
<pre>{{ platform }}</pre> <b-container>
</div> <b-spinner v-if="loading" class="spinner-centered" />
<section v-else-if="platform">
<portal to="pageTitle">
{{ platform.name }}
</portal>
<portal to="headerActions">
<b-button :to="{ name: 'platforms' }" class="mr-2">
All platforms
</b-button>
</portal>
<b-row class="mb-5">
<b-col v-for="game in platformGames" :key="game.id" cols="3">
<game-card-search :game="game" />
</b-col>
<b-button block class="mt-5 mb-5" @click="loadMoreGames">
Load more games
</b-button>
</b-row>
</section>
</b-container>
</section>
</template> </template>
<script> <script>
import merge from 'lodash.merge';
import GameCardSearch from '@/components/GameCards/GameCardSearch';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: { data() {
...mapGetters(['platforms']), return {
loading: false,
platforms: [],
platformGames: [],
offset: 0,
}
},
components: {
GameCardSearch,
},
computed: {
platform() { platform() {
return this.platforms.find(({ slug }) => slug === this.$route.params.slug); return this.platforms?.find(({ id }) => id == this.$route.params.id);
},
},
async mounted() {
this.platforms = await this.$store.dispatch('LOAD_IGDB_PLATFORMS');
this.loadGames();
},
methods: {
async loadGames() {
this.loading = this.offset === 0;
const data = `
fields platforms,slug,release_dates,rating,cover.image_id;
limit 50;
offset: ${this.offset};
sort name asc;
where release_dates.platform = ${this.platform.id};
`;
console.log('this.offset', this.offset);
if (this.offset === 0) {
this.platformGames = await this.$store.dispatch('IGDB', { path: 'games', data });
} else {
const games = await this.$store.dispatch('IGDB', { path: 'games', data });
console.log('games', games.length);
console.log(typeof games);
console.log('platformGames', this.platformGames);
console.log(typeof this.platformGames);
this.platformGames = merge(this.platformGames, games);
console.log('this.platformGames', this.platformGames);
}
this.loading = false;
},
async loadMoreGames() {
this.offset = this.platformGames.length;
this.loadGames();
// const data = `
// fields platforms,slug,release_dates,rating,cover.image_id;
// limit 50;
// offset: ${this.offset};
// sort name asc;
// where release_dates.platform = ${this.platform.id};
// `;
//
// const moreGames = await this.$store.dispatch('IGDB', { path: 'games', data });
//
// this.loading = false;
}, },
}, },
}; };
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -1,126 +1,26 @@
<!-- TODO: finish or kill it -->
<template lang="html"> <template lang="html">
<div> <div>
<b-button <b-button
variant="link" variant="link"
block
v-for="platform in platforms" v-for="platform in platforms"
:to="{ name: 'platform.page', params: { slug: platform.slug } }" :to="{ name: 'platform', params: { id: platform.id } }"
:key="platform.id" :key="platform.id"
> >
<img :src="`static/logos/platforms-new/${platform.slug}.png`" /> {{ platform.name }}
{{ platform.slug }} | {{ platform.id }}
</b-button> </b-button>
<!-- <img src="static/logos/platforms-new/3do.png" /> -->
<!-- <img src="static/logos/platforms-new/action-max.png" /> -->
<!-- <img src="static/logos/platforms-new/amiga-cd-3 2.png" /> -->
<!-- <img src="static/logos/platforms-new/arcadia-2001.png" /> -->
<!-- <img src="static/logos/platforms-new/astrocade.png" /> -->
<!-- <img src="static/logos/platforms-new/atari-2600.png" /> -->
<!-- <img src="static/logos/platforms-new/atari-5200.png" /> -->
<!-- <img src="static/logos/platforms-new/atari-7800.png" /> -->
<!-- <img src="static/logos/platforms-new/atari-xe.png" /> -->
<!-- <img src="static/logos/platforms-new/beena.png" /> -->
<!-- <img src="static/logos/platforms-new/cassette-vision.png" /> -->
<!-- <img src="static/logos/platforms-new/cd-i.png" /> -->
<!-- <img src="static/logos/platforms-new/channel-f.png" /> -->
<!-- <img src="static/logos/platforms-new/coleco-vision.png" /> -->
<!-- <img src="static/logos/platforms-new/commodore-cdtv.png" /> -->
<!-- <img src="static/logos/platforms-new/commodore64.png" /> -->
<!-- <img src="static/logos/platforms-new/cps-changer.png" /> -->
<!-- <img src="static/logos/platforms-new/creativision.png" /> -->
<!-- <img src="static/logos/platforms-new/dreamcast.png" /> -->
<!-- <img src="static/logos/platforms-new/family-computer.png" /> -->
<!-- <img src="static/logos/platforms-new/fm-towns-marty.png" /> -->
<!-- <img src="static/logos/platforms-new/game-wave.png" /> -->
<!-- <img src="static/logos/platforms-new/gamecube.png" /> -->
<!-- <img src="static/logos/platforms-new/genesis-32x.png" /> -->
<!-- <img src="static/logos/platforms-new/genesis.png" /> -->
<!-- <img src="static/logos/platforms-new/gx4000.png" /> -->
<!-- <img src="static/logos/platforms-new/halcyon.png" /> -->
<!-- <img src="static/logos/platforms-new/hyper-scan.png" /> -->
<!-- <img src="static/logos/platforms-new/imagination-machine.png" /> -->
<!-- <img src="static/logos/platforms-new/intellivision.png" /> -->
<!-- <img src="static/logos/platforms-new/interactive-vision.png" /> -->
<!-- <img src="static/logos/platforms-new/ique.png" /> -->
<!-- <img src="static/logos/platforms-new/jaguar-cd.png" /> -->
<!-- <img src="static/logos/platforms-new/jaguar.png" /> -->
<!-- <img src="static/logos/platforms-new/konix.png" /> -->
<!-- <img src="static/logos/platforms-new/laser-active.png" /> -->
<!-- <img src="static/logos/platforms-new/leisure-vision.png" /> -->
<!-- <img src="static/logos/platforms-new/loopy.png" /> -->
<!-- <img src="static/logos/platforms-new/mark-iii.png" /> -->
<!-- <img src="static/logos/platforms-new/mega-cd-ii.png" /> -->
<!-- <img src="static/logos/platforms-new/mega-cd.png" /> -->
<!-- <img src="static/logos/platforms-new/mega-drive.png" /> -->
<!-- <img src="static/logos/platforms-new/mega-ld.png" /> -->
<!-- <img src="static/logos/platforms-new/mp1000.png" /> -->
<!-- <img src="static/logos/platforms-new/my-vision.png" /> -->
<!-- <img src="static/logos/platforms-new/neo-geo-cd.png" /> -->
<!-- <img src="static/logos/platforms-new/neo-geo.png" /> -->
<!-- <img src="static/logos/platforms-new/nes.png" /> -->
<!-- <img src="static/logos/platforms-new/nintendo-64-dd.png" /> -->
<!-- <img src="static/logos/platforms-new/nintendo-64.png" /> -->
<!-- <img src="static/logos/platforms-new/nuon.png" /> -->
<!-- <img src="static/logos/platforms-new/odyssey2.png" /> -->
<!-- <img src="static/logos/platforms-new/pc-engine.png" /> -->
<!-- <img src="static/logos/platforms-new/pc-fx.png" /> -->
<!-- <img src="static/logos/platforms-new/picno.png" /> -->
<!-- <img src="static/logos/platforms-new/pico.png" /> -->
<!-- <img src="static/logos/platforms-new/pippin.png" /> -->
<!-- <img src="static/logos/platforms-new/playdia.png" /> -->
<!-- <img src="static/logos/platforms-new/playstation.png" /> -->
<!-- <img src="static/logos/platforms-new/ps-2.png" /> -->
<!-- <img src="static/logos/platforms-new/ps3.png" /> -->
<!-- <img src="static/logos/platforms-new/ps4.png" /> -->
<!-- <img src="static/logos/platforms-new/pv-1000.png" /> -->
<!-- <img src="static/logos/platforms-new/rca-studio-ii.png" /> -->
<!-- <img src="static/logos/platforms-new/satellaview.png" /> -->
<!-- <img src="static/logos/platforms-new/sega-cd.png" /> -->
<!-- <img src="static/logos/platforms-new/sega-master-system.png" /> -->
<!-- <img src="static/logos/platforms-new/sega-saturn.png" /> -->
<!-- <img src="static/logos/platforms-new/sg-1000.png" /> -->
<!-- <img src="static/logos/platforms-new/socrates.png" /> -->
<!-- <img src="static/logos/platforms-new/super-acan.png" /> -->
<!-- <img src="static/logos/platforms-new/super-cassette-vision.png" /> -->
<!-- <img src="static/logos/platforms-new/super-cd-rom.png" /> -->
<!-- <img src="static/logos/platforms-new/super-famicom.png" /> -->
<!-- <img src="static/logos/platforms-new/super-grafx.png" /> -->
<!-- <img src="static/logos/platforms-new/super-nintendo.png" /> -->
<!-- <img src="static/logos/platforms-new/super-vision-8000.png" /> -->
<!-- <img src="static/logos/platforms-new/switch.png" /> -->
<!-- <img src="static/logos/platforms-new/turbo-grafx-16.png" /> -->
<!-- <img src="static/logos/platforms-new/tutor.png" /> -->
<!-- <img src="static/logos/platforms-new/tv-boy.png" /> -->
<!-- <img src="static/logos/platforms-new/ultravision.png" /> -->
<!-- <img src="static/logos/platforms-new/v-flash.png" /> -->
<!-- <img src="static/logos/platforms-new/v-smile.png" /> -->
<!-- <img src="static/logos/platforms-new/vc4000.png" /> -->
<!-- <img src="static/logos/platforms-new/vectrex.png" /> -->
<!-- <img src="static/logos/platforms-new/video-art.png" /> -->
<!-- <img src="static/logos/platforms-new/video-challenger.png" /> -->
<!-- <img src="static/logos/platforms-new/video-driver.png" /> -->
<!-- <img src="static/logos/platforms-new/videopac.png" /> -->
<!-- <img src="static/logos/platforms-new/vis.png" /> -->
<!-- <img src="static/logos/platforms-new/wii-u.png" /> -->
<!-- <img src="static/logos/platforms-new/wii.png" /> -->
<!-- <img src="static/logos/platforms-new/xavix.png" /> -->
<!-- <img src="static/logos/platforms-new/xbox-360.png" /> -->
<!-- <img src="static/logos/platforms-new/xbox-one.png" /> -->
<!-- <img src="static/logos/platforms-new/xbox.png" /> -->
<!-- <img src="static/logos/platforms-new/zeebo.png" /> -->
<!-- <img src="static/logos/platforms-new/zemina.png" /> -->
<!-- <pre class="text-dark small">{{ platforms }}</pre> -->
</div> </div>
</template> </template>
<script> <script>
import orderby from 'lodash.orderby';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: { data() {
...mapGetters(['platforms']), return {
platforms: null,
}
}, },
mounted() { mounted() {
@ -129,10 +29,12 @@ export default {
methods: { methods: {
async loadPlatforms() { async loadPlatforms() {
await this.$store.dispatch('LOAD_IGDB_PLATFORMS') const platforms = await this.$store.dispatch('LOAD_IGDB_PLATFORMS')
.catch(() => { .catch(() => {
this.$bvToast.toast('There was an error loading platforms', { variant: 'error' }); this.$bvToast.toast('There was an error loading platforms', { variant: 'error' });
}); });
this.platforms = orderby(platforms, 'name');
}, },
}, },
}; };

View file

@ -8,8 +8,8 @@ const routes = [
}, },
}, },
{ {
path: '/platforms/:slug', path: '/p/:id',
name: 'platform.page', name: 'platform',
component: () => import(/* webpackChunkName: "platforms" */ '@/platforms/pages/PlatformPage'), component: () => import(/* webpackChunkName: "platforms" */ '@/platforms/pages/PlatformPage'),
}, },
]; ];

View file

@ -1,4 +1,4 @@
<!-- TODO: use custom login page --> <!-- TODO: use custom login page, modal? -->
<template lang="html"> <template lang="html">
<section> <section>
<b-container> <b-container>

View file

@ -1,11 +1,13 @@
<template lang="html"> <template lang="html">
<b-container> <section class="pb-5">
<game-boards /> <b-container>
<game-boards />
<!-- <div class="game-deals"> <!-- <div class="game-deals">
<twitter-feed twitter-user="wario64" /> <twitter-feed twitter-user="wario64" />
</div> --> </div> -->
</b-container> </b-container>
</section>
</template> </template>
<script> <script>

View file

@ -20,9 +20,14 @@
variant="light" variant="light"
class="mr-2" class="mr-2"
right right
no-caret
> >
<template #button-content> <template #button-content>
Filter <template v-if="selectedPlatforms.length">({{ selectedPlatforms.length }})</template> <i class="fa-solid fa-filter fa-fw" />
<b-badge v-if="selectedPlatforms.length">
{{ selectedPlatforms.length }}
</b-badge>
</template> </template>
<b-dropdown-item <b-dropdown-item
@ -45,45 +50,47 @@
</b-dropdown> </b-dropdown>
</portal> </portal>
<b-col cols="12" class="bg-light py-2 mb-3" v-if="activeBoard"> <b-col cols="12" class="py-2 mb-3" v-if="activeBoard">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="d-none d-sm-block"> <span class="d-none d-sm-block">
Add games to: Add games to:
</span> </span>
<b-button-group class="ml-sm-2"> <b-dropdown
<b-dropdown split
split variant="light"
variant="light" size="sm"
:split-to="{ name: 'board', params: { id: boardId } }" class="ml-2"
:text="activeBoard.name" :split-to="{ name: 'board', params: { id: boardId } }"
:text="activeBoard.name"
>
<b-dropdown-item
v-for="board in boards"
:key="board.id"
:disabled="!board.lists.length"
:to="{ name: 'search', query: { boardId: board.id, listIndex: 0, q: query } }"
> >
<b-dropdown-item {{ board.name }}
v-for="board in boards" </b-dropdown-item>
:key="board.id" </b-dropdown>
:disabled="!board.lists.length"
:to="{ name: 'search', query: { boardId: board.id, listIndex: 0, q: query } }"
>
{{ board.name }}
</b-dropdown-item>
</b-dropdown>
<b-dropdown <b-dropdown
v-if="activeBoardList" v-if="activeBoardList"
split split
variant="light" variant="light"
:split-to="{ name: 'board', params: { id: boardId } }" size="sm"
:text="activeBoardList.name" class="ml-2"
:split-to="{ name: 'board', params: { id: boardId } }"
:text="activeBoardList.name"
>
<b-dropdown-item
v-for="(list, listIndex) in activeBoard.lists"
:key="list.id"
:to="{ name: 'search', query: { boardId: activeBoard.id, listIndex, q: query } }"
> >
<b-dropdown-item {{ list.name }}
v-for="(list, listIndex) in activeBoard.lists" </b-dropdown-item>
:key="list.id" </b-dropdown>
:to="{ name: 'search', query: { boardId: activeBoard.id, listIndex, q: query } }"
>
{{ list.name }}
</b-dropdown-item>
</b-dropdown>
</b-button-group>
<b-button :to="{ name: 'search' }" class="ml-auto" variant="light"> <b-button :to="{ name: 'search' }" class="ml-auto" variant="light">
<i class="fas fa-times fa-fw" aria-hidden /> <i class="fas fa-times fa-fw" aria-hidden />
@ -104,6 +111,10 @@
> >
<game-card-search :game="game" /> <game-card-search :game="game" />
</b-col> </b-col>
<b-button @click="loadMoreResults">
load more
</b-button>
</b-form-row> </b-form-row>
<div <div
@ -207,10 +218,10 @@ export default {
}, },
async mounted() { async mounted() {
if (this.showEmptyState) { this.search();
} else { // if (this.showEmptyState) {
this.search(); // } else {
} // }
}, },
methods: { methods: {
@ -221,7 +232,10 @@ export default {
? `search "${this.query}";` ? `search "${this.query}";`
: ''; : '';
const data = `${search} fields platforms,slug,cover.image_id; limit 50;`; const filter = !this.query
? 'where rating >= 80;'
: '';
const data = `${search} fields platforms,slug,rating,cover.image_id; limit 50; ${filter}`;
this.searchResults = await this.$store.dispatch('IGDB', { path: 'games', data }); this.searchResults = await this.$store.dispatch('IGDB', { path: 'games', data });

View file

@ -3,7 +3,15 @@
<b-container> <b-container>
<portal to="pageTitle">Account</portal> <portal to="pageTitle">Account</portal>
<p>Your account</p> <p>Logged in as {{ user.displayName }} / {{ user.email }}</p>
<b-alert show class="field" variant="light">
<small>
<strong>User ID:</strong>
{{ user.uid }}
</small>
</b-alert>
<b-button <b-button
variant="light" variant="light"
@click="session_signOut" @click="session_signOut"
@ -11,10 +19,10 @@
Log out Log out
</b-button> </b-button>
<p class="mt-2">Delete account</p> <hr />
<b-button <b-button
variant="warning" variant="outline-dark"
@click="$bvModal.show('deleteAccount');" @click="$bvModal.show('deleteAccount');"
> >
Delete account Delete account

View file

@ -2,22 +2,21 @@
<section> <section>
<b-container> <b-container>
<b-row> <b-row>
<b-col cols="8"> <b-col>
<div class="d-flex align-items-center justify-content-between mb-2"> <div class="d-flex align-items-center justify-content-between mb-2 pt-2">
Boards Recent boards
<b-button <b-button
v-if="boards.length > 10" v-if="sortedBoards.length > 10"
size="sm" size="sm"
title="Boards" title="Boards"
variant="link"
:to="{ name: 'boards' }" :to="{ name: 'boards' }"
> >
View all boards View all boards
</b-button> </b-button>
</div> </div>
<b-row> <b-form-row>
<b-col <b-col
v-for="board in recentBoards" v-for="board in recentBoards"
:key="board.id" :key="board.id"
@ -25,44 +24,20 @@
sm="6" sm="6"
md="4" md="4"
lg="3" lg="3"
class="mb-4"
> >
<mini-board <mini-board
:board="board" :board="board"
style="height: 140px" class="mb-3"
style="height: 180px"
@click.native="$router.push({ name: 'board', params: { id: board.id } })" @click.native="$router.push({ name: 'board', params: { id: board.id } })"
/> />
</b-col> </b-col>
</b-row> </b-form-row>
</b-col>
<b-col cols="3 offset-1" class="text-center">
<b-avatar
class="d-flex ml-auto mr-auto mb-3 mt-5"
rounded
:src="avatarImage"
size="140px"
/>
<router-link
v-if="profile.userName"
:to="{ name: 'public.profile', params: { userName: profile.userName } }"
>
@{{ profile.userName }}
</router-link>
<b-button
v-else
:to="{ name: 'profile.settings' }"
variant="success"
>
Create profile <b-badge>New!</b-badge>
</b-button>
</b-col> </b-col>
</b-row> </b-row>
<b-row class="mt-3"> <b-row class="mt-3">
<b-col cols="6" md="3"> <b-col cols="12" sm="6" md="3">
<settings-card <settings-card
title="Wallpapers" title="Wallpapers"
description="Manage your wallpapers" description="Manage your wallpapers"
@ -70,7 +45,7 @@
@click.native="$router.push({ name: 'wallpapers' })" @click.native="$router.push({ name: 'wallpapers' })"
/> />
</b-col> </b-col>
<b-col cols="6" md="3"> <b-col cols="12" sm="6" md="3">
<settings-card <settings-card
title="Notes" title="Notes"
description="View all your notes" description="View all your notes"
@ -79,22 +54,65 @@
/> />
</b-col> </b-col>
<b-col cols="6" md="3"> <!-- <b-col cols="12" sm="6" md="3">
<b-card>
<h4>Tags</h4>
<b-button
v-for="({ textColor, bgColor, name, games }, index) in tags"
@click="$router.push({ name: 'tag.edit', params: { id: index } })"
rounded
size="sm"
class="mr-1 mb-1"
variant="transparent"
:style="`background-color: ${bgColor}; color: ${textColor}`"
:key="name"
>
{{ name }} {{ games.length ? `(${games.length})` : '' }}
</b-button>
</b-card>
</b-col> -->
<b-col cols="12" sm="6" md="3">
<settings-card <settings-card
title="Tags" title="Tags"
description="View all your tags" description="Manage your tags"
icon="fa-tags" icon="fa-tags"
@click.native="$router.push({ name: 'tags' })" @click.native="$router.push({ name: 'tags' })"
/> />
</b-col> </b-col>
<b-col cols="6" md="3"> <b-col cols="12" sm="6" md="3">
<settings-card <settings-card
title="Account" title="Account"
description="Manage your account" description="Manage your account"
icon="fa-user" icon="fa-user"
@click.native="$router.push({ name: 'account' })" @click.native="$router.push({ name: 'account' })"
/> />
<b-avatar
class="d-flex ml-auto mr-auto mb-3 mt-5"
rounded
:src="avatarImage"
size="120px"
/>
<b-button
v-if="profile.userName"
variant="secondary"
:to="{ name: 'public.profile', params: { userName: profile.userName } }"
>
@{{ profile.userName }}
</b-button>
<b-button
v-else
:to="{ name: 'profile.settings' }"
variant="success"
>
Create profile <b-badge>New!</b-badge>
</b-button>
</b-col> </b-col>
</b-row> </b-row>
@ -178,7 +196,7 @@ import MiniBoard from '@/components/Board/MiniBoard';
// import SteamSettingsPage from '@/pages/SteamSettingsPage'; // import SteamSettingsPage from '@/pages/SteamSettingsPage';
// import LanguageSettings from '@/components/Settings/LanguageSettings'; // import LanguageSettings from '@/components/Settings/LanguageSettings';
import { getImageThumbnail } from '@/utils'; import { getImageThumbnail } from '@/utils';
import { mapState } from 'vuex'; import { mapState, mapGetters } from 'vuex';
export default { export default {
components: { components: {
@ -192,7 +210,9 @@ export default {
data() { data() {
return { return {
popularGames: [115, 125174],
profile: {}, profile: {},
recentBoards: [],
avatarImage: null, avatarImage: null,
} }
}, },
@ -216,11 +236,8 @@ export default {
}, },
computed: { computed: {
...mapState(['user', 'releases', 'boards']), ...mapState(['user', 'releases', 'tags']),
...mapGetters(['sortedBoards']),
recentBoards() {
return this.boards.slice(0, 8);
},
latestRelease() { latestRelease() {
return this.releases?.[0]?.tag_name; return this.releases?.[0]?.tag_name;
@ -229,6 +246,11 @@ export default {
methods: { methods: {
async load() { async load() {
await this.$store.dispatch('LOAD_BOARDS');
await this.$store.dispatch('LOAD_TAGS');
this.recentBoards = this.sortedBoards.slice(0, 8);
this.profile = await this.$store.dispatch('LOAD_PROFILE').catch(() => null); this.profile = await this.$store.dispatch('LOAD_PROFILE').catch(() => null);
if (this.profile?.avatar) this.loadAvatarImage(); if (this.profile?.avatar) this.loadAvatarImage();
}, },

View file

@ -41,7 +41,6 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get(`${API_BASE}/platforms?token=${state.twitchToken.access_token}`) axios.get(`${API_BASE}/platforms?token=${state.twitchToken.access_token}`)
.then(({ data }) => { .then(({ data }) => {
commit('SET_PLATFORMS', data);
resolve(data); resolve(data);
}).catch(reject); }).catch(reject);
}); });
@ -120,8 +119,9 @@ export default {
const board = doc.data(); const board = doc.data();
return { return {
id: doc.id,
...board, ...board,
id: doc.id,
lastUpdated: board?.lastUpdated || 0,
}; };
}) })
: null; : null;

View file

@ -6,7 +6,7 @@ import presetHTML5 from '@bbob/preset-html5'
import orderby from 'lodash.orderby'; import orderby from 'lodash.orderby';
export default { export default {
sortedBoards: ({ boards }) => orderby(boards, 'name'), sortedBoards: ({ boards }) => orderby(boards, 'lastUpdated', 'desc'),
isBoardOwner: ({ board, user }) => { isBoardOwner: ({ board, user }) => {
return board?.owner === user?.uid; return board?.owner === user?.uid;

View file

@ -2,7 +2,6 @@ import Vue from 'vue';
// import { // import {
// // PLATFORM_CATEGORIES, // // PLATFORM_CATEGORIES,
// // EXCLUDED_PLATFORMS, // // EXCLUDED_PLATFORMS,
// // PLATFORM_BG_HEX,
// // PLATFORM_LOGO_FORMAT, // // PLATFORM_LOGO_FORMAT,
// // PLATFORM_NAME_OVERRIDES, // // PLATFORM_NAME_OVERRIDES,
// // POPULAR_PLATFORMS, // // POPULAR_PLATFORMS,
@ -65,28 +64,6 @@ export default {
state.profiles = profiles; state.profiles = profiles;
}, },
SET_PLATFORMS(state, platforms) {
// TODO: use getter instead to get fresh data right away instead of once per session
state.platforms = platforms;
// state.platforms = platforms
// .filter(({ id }) => !EXCLUDED_PLATFORMS.includes(id))
// .map((platform) => {
// const formattedPlatform = {
// id: platform.id,
// name: PLATFORM_NAME_OVERRIDES[platform.id] || platform.name,
// slug: platform.slug,
// category: PLATFORM_CATEGORIES[platform.category],
// popular: POPULAR_PLATFORMS.includes(platform.id),
// // categoryId: platform.category,
// generation: platform.generation || 0,
// bgHex: PLATFORM_BG_HEX[platform.id] || null,
// logoFormat: PLATFORM_LOGO_FORMAT[platform.id] || 'svg',
// };
//
// return formattedPlatform;
// });
},
SET_BOARD_GAMES(state, boardGames) { SET_BOARD_GAMES(state, boardGames) {
state.boardGames = boardGames; state.boardGames = boardGames;
}, },

View file

@ -1,4 +1,3 @@
// Colors
$primary: #3d6692 !default; $primary: #3d6692 !default;
$muted: #7d7f8c !default; $muted: #7d7f8c !default;
$danger: #C0392B !default; $danger: #C0392B !default;
@ -6,19 +5,10 @@ $warning: #F39C12 !default;
$success: #23cd69 !default; $success: #23cd69 !default;
$white: #f8f8ff !default; $white: #f8f8ff !default;
$black: #222222 !default; $black: #222222 !default;
$info: #0071bc !default; $info: #3C5275 !default;
$light: #dee4e7 !default; $light: #dee4e7 !default;
$dark: #37474f !default; $dark: #37474f !default;
$secondary: #b0bec5 !default; $secondary: #6D6969 !default;
$theme-colors: () !default; $theme-colors: () !default;
$theme-colors: map-merge(( $theme-colors: map-merge(( "primary": $primary, "secondary": $secondary, "success": $success, "info": $info, "warning": $warning, "danger": $danger, "light": $light, "dark": $dark, ), $theme-colors);
"primary": $primary,
"secondary": $secondary,
"success": $success,
"info": $info,
"warning": $warning,
"danger": $danger,
"light": $light,
"dark": $dark,
), $theme-colors);

View file

@ -26,8 +26,9 @@ $modal-header-border-width: 0;
$modal-footer-border-width: 0 !default; $modal-footer-border-width: 0 !default;
$modal-footer-padding: 0 !default; $modal-footer-padding: 0 !default;
$modal-header-padding: 1rem 1rem 0 !default; $modal-header-padding: 1rem 1rem 0 !default;
//
// $modal-xl: 1140px !default; // TODO: make xl modal take most of the screen
// $modal-xl: 100vw !default;
// $modal-lg: 800px !default; // $modal-lg: 800px !default;
$modal-md: 600px !default; $modal-md: 600px !default;
$modal-sm: 400px !default; $modal-sm: 400px !default;

793
yarn.lock

File diff suppressed because it is too large Load diff