58 | {
59 | products.map((product, index) => (
60 |
= lastRowFirstItemIndex ? 'product-list__card-wrapper--fixed' : ''}`}
62 | >
63 |
64 |
65 | ))
66 | }
67 |
68 | )
69 | }
70 |
71 | export default ProductList;
--------------------------------------------------------------------------------
/docs/api/interfaces/DummyMediaItem.md:
--------------------------------------------------------------------------------
1 | [**patreon-dl**](../README.md)
2 |
3 | ***
4 |
5 | [patreon-dl](../README.md) / DummyMediaItem
6 |
7 | # Interface: DummyMediaItem
8 |
9 | Defined in: [src/entities/MediaItem.ts:136](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L136)
10 |
11 | Minimal `MediaItem` typically used to represent media-type properties of elements, such
12 | as video thumbnails and campaign avatar / cover photos.
13 | As a `MediaItem` type, and hence also a `Downloadable` type, it can be used to create
14 | `MediaFilenameResolver` and `DownloadTask` instances.
15 |
16 | ## Extends
17 |
18 | - [`MediaLike`](MediaLike.md)
19 |
20 | ## Properties
21 |
22 | ### filename
23 |
24 | > **filename**: `null` \| `string`
25 |
26 | Defined in: [src/entities/MediaItem.ts:4](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L4)
27 |
28 | #### Inherited from
29 |
30 | [`MediaLike`](MediaLike.md).[`filename`](MediaLike.md#filename)
31 |
32 | ***
33 |
34 | ### id
35 |
36 | > **id**: `string`
37 |
38 | Defined in: [src/entities/MediaItem.ts:3](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L3)
39 |
40 | #### Inherited from
41 |
42 | [`MediaLike`](MediaLike.md).[`id`](MediaLike.md#id)
43 |
44 | ***
45 |
46 | ### mimeType
47 |
48 | > **mimeType**: `null` \| `string`
49 |
50 | Defined in: [src/entities/MediaItem.ts:5](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L5)
51 |
52 | #### Inherited from
53 |
54 | [`MediaLike`](MediaLike.md).[`mimeType`](MediaLike.md#mimetype)
55 |
56 | ***
57 |
58 | ### srcURLs
59 |
60 | > **srcURLs**: `Record`\<`string`, `string` \| `null`\>
61 |
62 | Defined in: [src/entities/MediaItem.ts:139](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L139)
63 |
64 | ***
65 |
66 | ### type
67 |
68 | > **type**: `"dummy"`
69 |
70 | Defined in: [src/entities/MediaItem.ts:137](https://github.com/patrickkfkan/patreon-dl/blob/99df673b92ef4ce3aebc4c26b094ba3e47fad262/src/entities/MediaItem.ts#L137)
71 |
72 | #### Overrides
73 |
74 | [`MediaLike`](MediaLike.md).[`type`](MediaLike.md#type)
75 |
--------------------------------------------------------------------------------
/src/browse/web/assets/styles/MediaGrid.scss:
--------------------------------------------------------------------------------
1 | .lg-video-poster {
2 | object-fit: contain;
3 | }
4 |
5 | .media-grid {
6 | display: grid;
7 | position: relative;
8 | max-height: 32em;
9 | gap: 1px;
10 | background: var(--bs-card-border-color);
11 | border-bottom: var(--bs-card-border-color);
12 |
13 | &--1 {
14 | grid-template-rows: 1fr;
15 | grid-template-columns: 1fr;
16 | grid-template-areas: 'a';
17 | }
18 |
19 | &--2 {
20 | grid-template-rows: 1fr;
21 | grid-template-columns: 1fr 1fr;
22 | grid-template-areas: 'a b';
23 | }
24 |
25 | &--3 {
26 | grid-template-rows: 1fr 1fr;
27 | grid-template-columns: 1fr 1fr;
28 | grid-template-areas: 'a b' 'a c';
29 | }
30 |
31 | &--4 {
32 | grid-template-rows: 1fr 1fr;
33 | grid-template-columns: 1fr 1fr;
34 | grid-template-areas: 'a b' 'c d';
35 | }
36 |
37 | &__item-wrapper {
38 | width: 100%;
39 | position: relative;
40 |
41 | &:nth-child(1) {
42 | grid-area: a;
43 | }
44 |
45 | &:nth-child(2) {
46 | grid-area: b;
47 | }
48 |
49 | &:nth-child(3) {
50 | grid-area: c;
51 | }
52 |
53 | &:nth-child(4) {
54 | grid-area: d;
55 | }
56 | }
57 |
58 | &__thumbnail-backdrop {
59 | width: 100%;
60 | height: 100%;
61 | position: absolute;
62 | background-size: cover;
63 | filter: blur(1em);
64 | transform: scale(1.5);
65 | }
66 |
67 | &__thumbnail-wrapper {
68 | width: 100%;
69 | height: 100%;
70 | z-index: 1000;
71 | cursor: pointer;
72 |
73 | &--video::before {
74 | content: '\e1c4';
75 | font-family: 'Material Icons Outlined';
76 | font-size: 4em;
77 | position: absolute;
78 | top: 50%;
79 | left: 50%;
80 | transform: translate(-50%, -50%);
81 | color: #fff;
82 | transition: opacity 150ms ease-in-out;
83 | opacity: 0.8;
84 | }
85 |
86 | &--video:hover::before {
87 | opacity: 1;
88 | }
89 | }
90 |
91 | &__thumbnail {
92 | width: 100%;
93 | height: 100%;
94 | object-fit: contain;
95 | }
96 |
97 | &__badge {
98 | position: absolute;
99 | font-size: 1em;
100 | bottom: 1em;
101 | left: 1em;
102 | z-index: 1001;
103 | }
104 | }
--------------------------------------------------------------------------------
/src/browse/web/components/CampaignCard.tsx:
--------------------------------------------------------------------------------
1 | import "../assets/styles/CampaignCard.scss";
2 | import { Link } from "react-router";
3 | import { Card, Stack } from "react-bootstrap";
4 | import RawDataExtractor from "../utils/RawDataExtractor";
5 | import { type CampaignWithCounts } from "../../types/Campaign";
6 | import MediaImage from "./MediaImage";
7 |
8 | interface CampaignCardProps {
9 | campaign: CampaignWithCounts;
10 | }
11 |
12 | const COUNT_ICONS: Partial