Skip to content

Commit 41dbb00

Browse files
Use CSS grid for card grid layout (#702)
* Use CSS grid for Catalogs * Use CSS grid for Items * Apply grid to item grids as well. (Fix problem 1, 2) * Force column flex layout (Fix problem 3 * Increase card min-width (fix problem 4) * Center preview that are shorter / narrower than available space (fix problem 6) * Left-align card content * Experiment: Align title, desc, meta on subgrid * Centralize CSS and some code for Cards, small improvements to styling, clean-up * Workaround for line-clamp --------- Co-authored-by: Matthias Mohr <[email protected]>
1 parent cc186d9 commit 41dbb00

File tree

9 files changed

+259
-358
lines changed

9 files changed

+259
-358
lines changed
Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,61 @@
1-
import { mapState } from 'vuex';
2-
3-
export default {
4-
props: {
5-
showThumbnail: {
6-
type: Boolean,
7-
default: true
8-
}
9-
},
10-
computed: {
11-
...mapState(['cardViewMode', 'crossOriginMedia', 'defaultThumbnailSize']),
12-
isList() {
13-
return this.data && !this.data.isItem() && this.cardViewMode === 'list';
14-
},
15-
hasImage() {
16-
return this.showThumbnail && this.thumbnail;
17-
},
18-
thumbnail() {
19-
if (this.data) {
20-
let thumbnails = this.data.getThumbnails(true, 'thumbnail');
21-
if (thumbnails.length > 0) {
22-
let t = thumbnails[0];
23-
let width, height;
24-
const shape = t.getMetadata('proj:shape');
25-
if (Array.isArray(shape) && shape.length === 2) {
26-
[height, width] = shape;
27-
}
28-
else if (Array.isArray(this.defaultThumbnailSize) && this.defaultThumbnailSize.length === 2) {
29-
[height, width] = this.defaultThumbnailSize;
30-
}
31-
return {
32-
src: t.getAbsoluteUrl(),
33-
alt: t.title,
34-
crossorigin: this.crossOriginMedia,
35-
width,
36-
height,
37-
placement: this.isList ? 'end' : 'top'
38-
};
39-
}
40-
}
41-
return null;
42-
}
43-
}
44-
};
1+
import { mapState } from 'vuex';
2+
import Utils from '../utils';
3+
import { STAC } from 'stac-js';
4+
5+
export default {
6+
props: {
7+
showThumbnail: {
8+
type: Boolean,
9+
default: true
10+
}
11+
},
12+
computed: {
13+
...mapState(['cardViewMode', 'crossOriginMedia', 'defaultThumbnailSize']),
14+
isList() {
15+
return this.data && !this.data.isItem() && this.cardViewMode === 'list';
16+
},
17+
hasImage() {
18+
return this.showThumbnail && this.thumbnail;
19+
},
20+
thumbnail() {
21+
if (this.data) {
22+
let thumbnails = this.data.getThumbnails(true, 'thumbnail');
23+
if (thumbnails.length > 0) {
24+
let t = thumbnails[0];
25+
let width, height;
26+
const shape = t.getMetadata('proj:shape');
27+
if (Array.isArray(shape) && shape.length === 2) {
28+
[height, width] = shape;
29+
}
30+
else if (Array.isArray(this.defaultThumbnailSize) && this.defaultThumbnailSize.length === 2) {
31+
[height, width] = this.defaultThumbnailSize;
32+
}
33+
return {
34+
src: t.getAbsoluteUrl(),
35+
alt: t.title,
36+
crossorigin: this.crossOriginMedia,
37+
width,
38+
height,
39+
placement: this.isList ? 'end' : 'top'
40+
};
41+
}
42+
}
43+
return null;
44+
},
45+
keywords() {
46+
if (this.data) {
47+
return this.data.getMetadata('keywords') || [];
48+
}
49+
return [];
50+
},
51+
isDeprecated() {
52+
return this.data instanceof STAC && Boolean(this.data.getMetadata('deprecated'));
53+
},
54+
hasDescription() {
55+
return this.data instanceof STAC && Utils.hasText(this.data.getMetadata('description'));
56+
},
57+
summarizeDescription() {
58+
return this.hasDescription ? Utils.summarizeMd(this.data.getMetadata('description'), 300) : '';
59+
}
60+
}
61+
};

src/components/Catalog.vue

Lines changed: 9 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
<template>
22
<b-card no-body :class="classes" v-visible.400="load" :img-placement="isList ? 'end' : undefined">
3-
<b-card-img v-if="hasImage" class="thumbnail" v-bind="thumbnail" lazy />
3+
<div class="card-img-wrapper">
4+
<b-card-img v-if="hasImage" class="thumbnail" v-bind="thumbnail" lazy />
5+
</div>
46
<b-card-body>
57
<b-card-title>
68
<StacLink :data="[data, catalog]" class="stretched-link" />
79
</b-card-title>
8-
<b-card-text v-if="data && (fileFormats.length > 0 || data.description || data.deprecated)" class="intro">
9-
<b-badge v-if="data.deprecated" variant="warning" class="me-1 mt-1 deprecated">{{ $t('deprecated') }}</b-badge>
10+
<b-card-text v-if="fileFormats.length > 0 || hasDescription || isDeprecated" class="intro">
11+
<b-badge v-if="isDeprecated" variant="warning" class="me-1 mt-1 deprecated">{{ $t('deprecated') }}</b-badge>
1012
<b-badge v-for="format in fileFormats" :key="format" variant="secondary" class="me-1 mt-1 fileformat">{{ formatMediaType(format) }}</b-badge>
11-
{{ summarizeDescription(data.description) }}
13+
{{ summarizeDescription }}
1214
</b-card-text>
13-
<Keywords v-if="showKeywordsInCatalogCards && keywords.length > 0" :keywords="keywords" variant="primary" :center="!isList" />
15+
<Keywords v-if="showKeywordsInCatalogCards && keywords.length > 0" :keywords="keywords" variant="primary" />
1416
<b-card-text v-if="temporalExtent" class="datetime"><small v-html="temporalExtent" /></b-card-text>
1517
</b-card-body>
1618
<b-card-footer>
@@ -24,11 +26,10 @@ import { defineAsyncComponent } from 'vue';
2426
import { mapState, mapGetters } from 'vuex';
2527
import FileFormatsMixin from './FileFormatsMixin';
2628
import StacFieldsMixin from './StacFieldsMixin';
27-
import ThumbnailCardMixin from './ThumbnailCardMixin';
29+
import CardMixin from './CardMixin';
2830
import StacLink from './StacLink.vue';
2931
import { STAC } from 'stac-js';
3032
import { formatMediaType, formatTemporalExtent } from '@radiantearth/stac-fields/formatters';
31-
import Utils from '../utils';
3233
import { BCard, BCardBody, BCardFooter, BCardImg, BCardText, BCardTitle } from 'bootstrap-vue-next';
3334
3435
export default {
@@ -45,7 +46,7 @@ export default {
4546
},
4647
mixins: [
4748
FileFormatsMixin,
48-
ThumbnailCardMixin,
49+
CardMixin,
4950
StacFieldsMixin({ formatTemporalExtent })
5051
],
5152
props: {
@@ -84,12 +85,6 @@ export default {
8485
}
8586
}
8687
return null;
87-
},
88-
keywords() {
89-
if (this.data) {
90-
return this.data.getMetadata('keywords') || [];
91-
}
92-
return [];
9388
}
9489
},
9590
methods: {
@@ -99,112 +94,9 @@ export default {
9994
}
10095
this.$store.commit(visible ? 'queue' : 'unqueue', this.catalog.getAbsoluteUrl());
10196
},
102-
summarizeDescription(text) {
103-
return Utils.summarizeMd(text, 300);
104-
},
10597
formatMediaType(value) {
10698
return formatMediaType(value, null, {shorten: true});
10799
}
108100
}
109101
};
110102
</script>
111-
112-
<style lang="scss">
113-
@import '~bootstrap/scss/mixins';
114-
@import '../theme/variables.scss';
115-
116-
#stac-browser {
117-
.catalog-card {
118-
&.deprecated {
119-
opacity: 0.5;
120-
121-
&:hover {
122-
opacity: 1;
123-
}
124-
}
125-
126-
.card-body, .card-footer {
127-
position: relative;
128-
}
129-
.card-footer:empty {
130-
display: none;
131-
}
132-
133-
/* Card image base styling */
134-
.card-img-top,
135-
.card-img-end,
136-
.card-img-start {
137-
object-fit: contain;
138-
object-position: center;
139-
}
140-
141-
.intro {
142-
display: -webkit-box;
143-
-webkit-line-clamp: 3;
144-
line-clamp: 3;
145-
-webkit-box-orient: vertical;
146-
overflow: hidden;
147-
overflow-wrap: anywhere;
148-
text-align: left;
149-
}
150-
&.has-extent {
151-
.intro {
152-
margin-bottom: 0.5rem;
153-
}
154-
}
155-
.datetime {
156-
color: $secondary;
157-
}
158-
.badge.deprecated {
159-
text-transform: uppercase;
160-
}
161-
}
162-
.card-list {
163-
.catalog-card {
164-
box-sizing: border-box;
165-
margin: 0.5em 0;
166-
display: flex;
167-
168-
.card-img-end {
169-
min-height: 100px;
170-
height: 100%;
171-
max-height: 8.5rem;
172-
max-width: 33%;
173-
object-fit: contain;
174-
object-position: center right;
175-
}
176-
.card-footer {
177-
min-width: 175px;
178-
max-width: 175px;
179-
border-top: 0;
180-
}
181-
.intro {
182-
-webkit-line-clamp: 2;
183-
line-clamp: 2;
184-
}
185-
}
186-
}
187-
.card-columns {
188-
.catalog-card {
189-
box-sizing: border-box;
190-
margin-top: 0.5em 0;
191-
text-align: center;
192-
193-
&.queued {
194-
min-height: 10rem;
195-
}
196-
.card-img-top {
197-
width: auto;
198-
height: auto;
199-
max-width: 100%;
200-
max-height: 300px;
201-
object-fit: contain;
202-
object-position: center;
203-
}
204-
.card-title {
205-
text-align: center;
206-
}
207-
}
208-
}
209-
}
210-
</style>

src/components/Catalogs.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<b-alert v-if="hasSearchCritera && catalogView.length === 0" variant="warning" class="mt-2" show>{{ $t('catalogs.noMatches') }}</b-alert>
2525
<section class="list">
2626
<Loading v-if="loading" fill top />
27-
<div :class="view === 'list' ? 'card-list' : 'card-columns'">
27+
<div :class="view === 'list' ? 'card-list' : 'card-grid'">
2828
<Catalog v-for="catalog in catalogView" :catalog="catalog" :key="catalog.href">
2929
<template #footer="{data}">
3030
<slot name="catalogFooter" :data="data" />

0 commit comments

Comments
 (0)