Compare commits
3 commits
a4e6f88e07
...
e8a34379f3
| Author | SHA1 | Date | |
|---|---|---|---|
| e8a34379f3 | |||
| 4229001572 | |||
| 88e37bfee8 |
15 changed files with 2305 additions and 917 deletions
|
|
@ -105,6 +105,7 @@ export default class DatasetController {
|
||||||
'reviewed',
|
'reviewed',
|
||||||
'rejected_editor',
|
'rejected_editor',
|
||||||
'rejected_reviewer',
|
'rejected_reviewer',
|
||||||
|
'rejected_to_reviewer',
|
||||||
])
|
])
|
||||||
.where('account_id', user.id)
|
.where('account_id', user.id)
|
||||||
.preload('titles')
|
.preload('titles')
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import vine from '@vinejs/vine';
|
||||||
|
|
||||||
export const createProjectValidator = vine.compile(
|
export const createProjectValidator = vine.compile(
|
||||||
vine.object({
|
vine.object({
|
||||||
label: vine.string().trim().minLength(1).maxLength(50),
|
label: vine.string().trim().minLength(1).maxLength(50) .regex(/^[a-z0-9-]+$/),
|
||||||
name: vine
|
name: vine
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255)
|
.maxLength(255)
|
||||||
.regex(/^[a-z0-9-]+$/),
|
.regex(/^[a-zA-Z0-9äöüßÄÖÜ\s-]+$/),
|
||||||
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -22,7 +22,7 @@ export const updateProjectValidator = vine.compile(
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255)
|
.maxLength(255)
|
||||||
.regex(/^[a-z0-9-]+$/),
|
.regex(/^[a-zA-Z0-9äöüßÄÖÜ\s-]+$/),
|
||||||
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
18
components.d.ts
vendored
18
components.d.ts
vendored
|
|
@ -11,3 +11,21 @@ declare module '@vue/runtime-core' {
|
||||||
NInput: (typeof import('naive-ui'))['NInput'];
|
NInput: (typeof import('naive-ui'))['NInput'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// types/leaflet-src-dom-DomEvent.d.ts
|
||||||
|
declare module 'leaflet/src/dom/DomEvent' {
|
||||||
|
export type DomEventHandler = (e?: any) => void;
|
||||||
|
|
||||||
|
// Attach event listeners. `obj` can be any DOM node or object with event handling.
|
||||||
|
export function on(obj: any, types: string, fn: DomEventHandler, context?: any): void;
|
||||||
|
|
||||||
|
// Detach event listeners.
|
||||||
|
export function off(obj: any, types: string, fn?: DomEventHandler, context?: any): void;
|
||||||
|
|
||||||
|
// Prevent default on native events
|
||||||
|
export function preventDefault(ev?: Event | undefined): void;
|
||||||
|
|
||||||
|
// Optional: other helpers you might need later
|
||||||
|
export function stopPropagation(ev?: Event | undefined): void;
|
||||||
|
export function stop(ev?: Event | undefined): void;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import type { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
|
// import type { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
|
||||||
import type { LatLngExpression } from 'leaflet/src/geo/LatLng';
|
// import type { LatLngExpression } from 'leaflet/src/geo/LatLng';
|
||||||
import type { Layer } from 'leaflet/src/layer/Layer';
|
// import type { Layer } from 'leaflet/src/layer/Layer';
|
||||||
import type { CRS } from 'leaflet/src/geo/crs/CRS';
|
// import type { CRS } from 'leaflet/src/geo/crs/CRS';
|
||||||
|
import type { LatLngBoundsExpression } from 'leaflet';
|
||||||
|
import type { LatLngExpression } from 'leaflet';
|
||||||
|
import type { Layer } from 'leaflet';
|
||||||
|
import type { CRS } from 'leaflet';
|
||||||
|
|
||||||
export interface MapOptions {
|
export interface MapOptions {
|
||||||
preferCanvas?: boolean | undefined;
|
preferCanvas?: boolean | undefined;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { svg } from 'leaflet/src/layer/vector/SVG';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
|
import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
|
||||||
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
||||||
// import { TileLayer } from 'leaflet/src/layer/tile/TileLayer';
|
|
||||||
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
||||||
import DrawControlComponent from '@/Components/Map/draw.component.vue';
|
import DrawControlComponent from '@/Components/Map/draw.component.vue';
|
||||||
import ZoomControlComponent from '@/Components/Map/zoom.component.vue';
|
import ZoomControlComponent from '@/Components/Map/zoom.component.vue';
|
||||||
|
|
@ -17,14 +16,7 @@ import { LayerGroup } from 'leaflet/src/layer/LayerGroup';
|
||||||
import { OpensearchDocument } from '@/Dataset';
|
import { OpensearchDocument } from '@/Dataset';
|
||||||
|
|
||||||
Map.include({
|
Map.include({
|
||||||
// @namespace Map; @method getRenderer(layer: Path): Renderer
|
|
||||||
// Returns the instance of `Renderer` that should be used to render the given
|
|
||||||
// `Path`. It will ensure that the `renderer` options of the map and paths
|
|
||||||
// are respected, and that the renderers do exist on the map.
|
|
||||||
getRenderer: function (layer) {
|
getRenderer: function (layer) {
|
||||||
// @namespace Path; @option renderer: Renderer
|
|
||||||
// Use this specific instance of `Renderer` for this path. Takes
|
|
||||||
// precedence over the map's [default renderer](#map-renderer).
|
|
||||||
var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
|
var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
|
||||||
|
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
|
|
@ -51,21 +43,18 @@ Map.include({
|
||||||
},
|
},
|
||||||
|
|
||||||
_createRenderer: function (options) {
|
_createRenderer: function (options) {
|
||||||
// @namespace Map; @option preferCanvas: Boolean = false
|
|
||||||
// Whether `Path`s should be rendered on a `Canvas` renderer.
|
|
||||||
// By default, all `Path`s are rendered in a `SVG` renderer.
|
|
||||||
return (this.options.preferCanvas && canvas(options)) || svg(options);
|
return (this.options.preferCanvas && canvas(options)) || svg(options);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
|
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
|
||||||
const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
||||||
const OPENSEARCH_HOST = 'https://catalog.geosphere.at';
|
const OPENSEARCH_HOST = 'https://catalog.geosphere.at';
|
||||||
// const OPENSEARCH_HOST = `${process.env.OPENSEARCH_HOST}`;
|
|
||||||
// const OPENSEARCH_HOST = `http://${process.env.OPENSEARCH_PUBLIC_HOST}`;
|
|
||||||
let map: Map;
|
let map: Map;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dheckable: Boolean,
|
checkable: Boolean,
|
||||||
datasets: {
|
datasets: {
|
||||||
type: Array<OpensearchDocument>,
|
type: Array<OpensearchDocument>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
|
|
@ -89,10 +78,7 @@ const items = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.datasets;
|
return props.datasets;
|
||||||
},
|
},
|
||||||
// setter
|
|
||||||
set(value) {
|
set(value) {
|
||||||
// Note: we are using destructuring assignment syntax here.
|
|
||||||
|
|
||||||
props.datasets.length = 0;
|
props.datasets.length = 0;
|
||||||
props.datasets.push(...value);
|
props.datasets.push(...value);
|
||||||
},
|
},
|
||||||
|
|
@ -103,15 +89,13 @@ const fitBounds: LatLngBoundsExpression = [
|
||||||
[49.0390742051, 16.9796667823],
|
[49.0390742051, 16.9796667823],
|
||||||
];
|
];
|
||||||
|
|
||||||
// const mapId = 'map';
|
|
||||||
const drawControl: Ref<DrawControlComponent | null> = ref(null);
|
const drawControl: Ref<DrawControlComponent | null> = ref(null);
|
||||||
const southWest = ref(null);
|
const southWest = ref(null);
|
||||||
const northEast = ref(null);
|
const northEast = ref(null);
|
||||||
const mapService = MapService();
|
const mapService = MapService();
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const filterLayerGroup = new LayerGroup();
|
const filterLayerGroup = new LayerGroup();
|
||||||
// Replace with your actual data
|
|
||||||
// const datasets: Ref<OpensearchDocument[]> = ref([]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initMap();
|
initMap();
|
||||||
|
|
@ -122,7 +106,6 @@ onUnmounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const initMap = async () => {
|
const initMap = async () => {
|
||||||
// init leaflet map
|
|
||||||
map = new Map('map', props.mapOptions);
|
map = new Map('map', props.mapOptions);
|
||||||
mapService.setMap(props.mapId, map);
|
mapService.setMap(props.mapId, map);
|
||||||
map.scrollWheelZoom.disable();
|
map.scrollWheelZoom.disable();
|
||||||
|
|
@ -140,11 +123,6 @@ const initMap = async () => {
|
||||||
layers: 'OSM-WMS',
|
layers: 'OSM-WMS',
|
||||||
});
|
});
|
||||||
|
|
||||||
// let baseAt = new TileLayer('https://{s}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png', {
|
|
||||||
// subdomains: ['maps', 'maps1', 'maps2', 'maps3', 'maps4'],
|
|
||||||
// attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
|
|
||||||
// });
|
|
||||||
|
|
||||||
let layerOptions = {
|
let layerOptions = {
|
||||||
label: DEFAULT_BASE_LAYER_NAME,
|
label: DEFAULT_BASE_LAYER_NAME,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
|
@ -153,62 +131,15 @@ const initMap = async () => {
|
||||||
layerOptions.layer.addTo(map);
|
layerOptions.layer.addTo(map);
|
||||||
|
|
||||||
map.on('Draw.Event.CREATED', handleDrawEventCreated);
|
map.on('Draw.Event.CREATED', handleDrawEventCreated);
|
||||||
|
|
||||||
// // const query = {
|
|
||||||
// // query: {
|
|
||||||
// // term: {
|
|
||||||
// // id: "103"
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // };
|
|
||||||
// // to do : call extra method:
|
|
||||||
// const query = {
|
|
||||||
// // q: 'id:103'
|
|
||||||
// // q: 'author:"Iglseder, Christoph" OR title:"Datensatz"',
|
|
||||||
// // q: 'author:"Iglseder"',
|
|
||||||
// q: '*',
|
|
||||||
// _source: 'author,bbox_xmin,bbox_xmax,bbox_ymin,bbox_ymax,abstract,title',
|
|
||||||
// size: 1000
|
|
||||||
// // qf:"title^3 author^2 subject^1",
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// let response = await axios({
|
|
||||||
// method: 'GET',
|
|
||||||
// url: OPEN_SEARCH_HOST + '/tethys-records/_search',
|
|
||||||
// headers: { 'Content-Type': 'application/json' },
|
|
||||||
// params: query
|
|
||||||
// });
|
|
||||||
// // Loop through the hits in the response
|
|
||||||
// response.data.hits.hits.forEach(hit => {
|
|
||||||
// // Get the geo_location attribute
|
|
||||||
// // var geo_location = hit._source.geo_location;
|
|
||||||
// let xMin = hit._source.bbox_xmin;
|
|
||||||
// let xMax = hit._source.bbox_xmax;
|
|
||||||
// let yMin = hit._source.bbox_ymin;
|
|
||||||
// let yMax = hit._source.bbox_ymax;
|
|
||||||
// var bbox: LatLngBoundsExpression = [[yMin, xMin], [yMax, xMax]];
|
|
||||||
// // Parse the WKT string to get the bounding box coordinates
|
|
||||||
// // var bbox = wktToBbox(geo_location);
|
|
||||||
|
|
||||||
// // // Add the bounding box to the map as a rectangle
|
|
||||||
// new Rectangle(bbox, { color: "#ff7800", weight: 1 }).addTo(map);
|
|
||||||
// // console.log(hit._source);
|
|
||||||
// });
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error(error);
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrawEventCreated = async (event) => {
|
const handleDrawEventCreated = async (event) => {
|
||||||
|
isLoading.value = true;
|
||||||
filterLayerGroup.clearLayers();
|
filterLayerGroup.clearLayers();
|
||||||
items.value = [];
|
items.value = [];
|
||||||
|
|
||||||
let layer = event.layer;
|
let layer = event.layer;
|
||||||
let bounds = layer.getBounds();
|
let bounds = layer.getBounds();
|
||||||
// coverage.x_min = bounds.getSouthWest().lng;
|
|
||||||
// coverage.y_min = bounds.getSouthWest().lat;
|
|
||||||
// coverage.x_max = bounds.getNorthEast().lng;
|
|
||||||
// coverage.y_max = bounds.getNorthEast().lat;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response = await axios({
|
let response = await axios({
|
||||||
|
|
@ -225,7 +156,6 @@ const handleDrawEventCreated = async (event) => {
|
||||||
filter: {
|
filter: {
|
||||||
geo_shape: {
|
geo_shape: {
|
||||||
geo_location: {
|
geo_location: {
|
||||||
// replace 'location' with your geo-point field name
|
|
||||||
shape: {
|
shape: {
|
||||||
type: 'envelope',
|
type: 'envelope',
|
||||||
coordinates: [
|
coordinates: [
|
||||||
|
|
@ -237,16 +167,12 @@ const handleDrawEventCreated = async (event) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// _source: 'author,bbox_xmin,bbox_xmax,bbox_ymin,bbox_ymax,abstract,title',
|
|
||||||
// "size": 1000
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Loop through the hits in the response
|
|
||||||
response.data.hits.hits.forEach((hit) => {
|
response.data.hits.hits.forEach((hit) => {
|
||||||
// Get the geo_location attribute
|
|
||||||
// var geo_location = hit._source.geo_location;
|
|
||||||
let xMin = hit._source.bbox_xmin;
|
let xMin = hit._source.bbox_xmin;
|
||||||
let xMax = hit._source.bbox_xmax;
|
let xMax = hit._source.bbox_xmax;
|
||||||
let yMin = hit._source.bbox_ymin;
|
let yMin = hit._source.bbox_ymin;
|
||||||
|
|
@ -255,46 +181,255 @@ const handleDrawEventCreated = async (event) => {
|
||||||
[yMin, xMin],
|
[yMin, xMin],
|
||||||
[yMax, xMax],
|
[yMax, xMax],
|
||||||
];
|
];
|
||||||
// Parse the WKT string to get the bounding box coordinates
|
|
||||||
// var bbox = wktToBbox(geo_location);
|
|
||||||
|
|
||||||
// // Add the bounding box to the map as a rectangle
|
let rect = new Rectangle(bbox, {
|
||||||
let rect = new Rectangle(bbox, { color: '#ff7800', weight: 1 });
|
color: '#65DC21',
|
||||||
|
weight: 2,
|
||||||
|
fillColor: '#65DC21',
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
className: 'animated-rectangle',
|
||||||
|
});
|
||||||
filterLayerGroup.addLayer(rect);
|
filterLayerGroup.addLayer(rect);
|
||||||
// add to result list
|
|
||||||
items.value.push(hit._source);
|
items.value.push(hit._source);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<div id="map" class="map-container mt-6 mb-6 rounded-2xl py-12 px-6 text-center dark:bg-slate-900 bg-white">
|
<div class="map-container-wrapper">
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div v-if="isLoading" class="loading-overlay">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p class="loading-text">Searching datasets...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Instructions Banner -->
|
||||||
|
<div class="map-instructions">
|
||||||
|
<svg class="instruction-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 16v-4M12 8h.01" />
|
||||||
|
</svg>
|
||||||
|
<p class="instruction-text">
|
||||||
|
<strong>Tip:</strong> Use the drawing tool to select an area on the map and discover datasets
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map" class="map-container">
|
||||||
<ZoomControlComponent ref="zoomControl" :mapId="mapId"></ZoomControlComponent>
|
<ZoomControlComponent ref="zoomControl" :mapId="mapId"></ZoomControlComponent>
|
||||||
<DrawControlComponent ref="drawControl" :preserve="false" :mapId="mapId" :southWest="southWest"
|
<DrawControlComponent ref="drawControl" :preserve="false" :mapId="mapId" :southWest="southWest" :northEast="northEast">
|
||||||
:northEast="northEast">
|
|
||||||
</DrawControlComponent>
|
</DrawControlComponent>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
<style scoped lang="css">
|
.map-container-wrapper {
|
||||||
/* .leaflet-container {
|
position: relative;
|
||||||
height: 600px;
|
border-radius: 1rem;
|
||||||
width: 100%;
|
overflow: hidden;
|
||||||
background-color: transparent;
|
background: white;
|
||||||
outline-offset: 1px;
|
box-shadow:
|
||||||
} */
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
.leaflet-container {
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
height: 600px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-container .leaflet-pane {
|
.dark .map-container-wrapper {
|
||||||
z-index: 30!important;
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map Instructions Banner */
|
||||||
|
.map-instructions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, rgba(101, 220, 33, 0.1) 0%, rgba(53, 124, 6, 0.1) 100%);
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .map-instructions {
|
||||||
|
background: linear-gradient(135deg, rgba(101, 220, 33, 0.2) 0%, rgba(53, 124, 6, 0.2) 100%);
|
||||||
|
border-bottom-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-icon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
color: #65dc21;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4b5563;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .instruction-text {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-text strong {
|
||||||
|
color: #65dc21;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Overlay */
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .loading-overlay {
|
||||||
|
background: rgba(31, 41, 55, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border: 4px solid #e5e7eb;
|
||||||
|
border-top-color: #65dc21;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .loading-spinner {
|
||||||
|
border-color: #374151;
|
||||||
|
border-top-color: #65dc21;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #65dc21;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map Container */
|
||||||
|
.map-container {
|
||||||
|
position: relative;
|
||||||
|
height: 600px;
|
||||||
|
width: 100%;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .map-container {
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaflet Overrides */
|
||||||
|
:deep(.leaflet-container) {
|
||||||
|
height: 600px;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-container .leaflet-pane) {
|
||||||
|
z-index: 30 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Rectangle Styling */
|
||||||
|
:deep(.animated-rectangle) {
|
||||||
|
animation: pulseRectangle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulseRectangle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Control Enhancements */
|
||||||
|
:deep(.leaflet-control) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-bar a) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-bar a:hover) {
|
||||||
|
background: #65dc21;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-draw-toolbar a) {
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark :deep(.leaflet-draw-toolbar a) {
|
||||||
|
background: #374151;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-draw-toolbar a:hover) {
|
||||||
|
background: #65dc21;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup Enhancements */
|
||||||
|
:deep(.leaflet-popup-content-wrapper) {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-popup-tip) {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.map-container {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-instructions {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-text {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.map-container {
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,14 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="drawControl" class="gba-control-draw btn-group-vertical map-control">
|
<div class="draw-control-container">
|
||||||
<!-- <button type="button" class="button is-light is-small" (click)="locateUser()" [ngClass]="isToggled ? 'is-primary': 'is-active'">
|
<button
|
||||||
<fa-icon [icon]="faSearchLocation"></fa-icon>
|
ref="drawButton"
|
||||||
</button> -->
|
class="draw-button"
|
||||||
<!-- -->
|
:class="{ 'is-active': enabled }"
|
||||||
<button ref="inputDraw"
|
type="button"
|
||||||
class="inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black border-teal-50 hover:bg-gray-200 text-sm p-1"
|
@click.stop.prevent="toggleDraw"
|
||||||
type="button" :class="[_enabled ? 'cursor-not-allowed bg-cyan-200' : 'bg-teal-50 is-active']"
|
:aria-label="enabled ? 'Stop drawing' : 'Start drawing'"
|
||||||
@click.prevent="toggleDraw">
|
:aria-pressed="enabled"
|
||||||
<BaseIcon v-if="mdiDrawPen" :path="mdiDrawPen" />
|
>
|
||||||
|
<!-- Icon changes based on state -->
|
||||||
|
<!-- <BaseIcon
|
||||||
|
v-if="enabled"
|
||||||
|
:path="mdiClose"
|
||||||
|
:size="20"
|
||||||
|
/> -->
|
||||||
|
<BaseIcon
|
||||||
|
:path="mdiVectorRectangle"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Status indicator -->
|
||||||
|
<!-- <span class="draw-status-badge" :class="{ 'is-active': enabled }">
|
||||||
|
{{ enabled ? 'Active' : 'Draw' }}
|
||||||
|
</span> -->
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,16 +32,14 @@
|
||||||
import { Component, Vue, Prop } from 'vue-facing-decorator';
|
import { Component, Vue, Prop } from 'vue-facing-decorator';
|
||||||
|
|
||||||
import BaseIcon from '@/Components/BaseIcon.vue';
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import { mdiDrawPen } from '@mdi/js';
|
import { mdiVectorRectangle, mdiClose } from '@mdi/js';
|
||||||
import { MapService } from '@/Stores/map.service';
|
import { MapService } from '@/Stores/map.service';
|
||||||
import { Map } from 'leaflet/src/map/index';
|
import { Map } from 'leaflet';
|
||||||
// import { LayerGroup } from 'leaflet/src/layer/LayerGroup';
|
|
||||||
// import { LatLngBounds, Rectangle } from 'leaflet';
|
|
||||||
import { on, off, preventDefault } from 'leaflet/src/dom/DomEvent';
|
import { on, off, preventDefault } from 'leaflet/src/dom/DomEvent';
|
||||||
import { Rectangle } from 'leaflet/src/layer/vector/Rectangle';
|
|
||||||
import { LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
import { Rectangle } from 'leaflet';
|
||||||
|
import { LatLngBounds } from 'leaflet';
|
||||||
import { LatLng } from 'leaflet';
|
import { LatLng } from 'leaflet';
|
||||||
import { LeafletMouseEvent } from 'leaflet';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'draw-control',
|
name: 'draw-control',
|
||||||
|
|
@ -34,19 +47,19 @@ import { LeafletMouseEvent } from 'leaflet';
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DrawControlComponent extends Vue {
|
export class DrawControlComponent extends Vue {
|
||||||
public TYPE = 'rectangle';
|
public TYPE = 'rectangle';
|
||||||
mdiDrawPen = mdiDrawPen;
|
mdiVectorRectangle = mdiVectorRectangle;
|
||||||
// private featuresLayer;
|
mdiClose = mdiClose;
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
shapeOptions: {
|
shapeOptions: {
|
||||||
stroke: true,
|
stroke: true,
|
||||||
color: '#22C55E',
|
color: '#65DC21',
|
||||||
weight: 4,
|
weight: 4,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
fill: true,
|
fill: true,
|
||||||
fillColor: '#22C55E', //same as color by default
|
fillColor: '#65DC21',
|
||||||
fillOpacity: 0.2,
|
fillOpacity: 0.2,
|
||||||
clickable: true,
|
clickable: true,
|
||||||
},
|
},
|
||||||
|
|
@ -56,7 +69,6 @@ export default class DrawControlComponent extends Vue {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Prop() public mapId: string;
|
@Prop() public mapId: string;
|
||||||
// @Prop() public map: Map;
|
|
||||||
@Prop public southWest: LatLng;
|
@Prop public southWest: LatLng;
|
||||||
@Prop public northEast: LatLng;
|
@Prop public northEast: LatLng;
|
||||||
@Prop({
|
@Prop({
|
||||||
|
|
@ -65,13 +77,17 @@ export default class DrawControlComponent extends Vue {
|
||||||
public preserve: boolean;
|
public preserve: boolean;
|
||||||
|
|
||||||
mapService = MapService();
|
mapService = MapService();
|
||||||
public _enabled: boolean;
|
private _enabled: boolean;
|
||||||
private _map: Map;
|
private _map: Map;
|
||||||
private _isDrawing: boolean = false;
|
private _isDrawing: boolean = false;
|
||||||
private _startLatLng: LatLng;
|
private _startLatLng: LatLng;
|
||||||
private _mapDraggable: boolean;
|
private _mapDraggable: boolean;
|
||||||
private _shape: Rectangle | undefined;
|
private _shape: Rectangle | undefined;
|
||||||
|
|
||||||
|
get enabled() {
|
||||||
|
return this._enabled;
|
||||||
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
if (this._enabled) {
|
if (this._enabled) {
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -93,49 +109,35 @@ export default class DrawControlComponent extends Vue {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled() {
|
// enabled() {
|
||||||
return !!this._enabled;
|
// return !!this._enabled;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// @Ref('inputDraw') private _inputDraw: HTMLElement;
|
|
||||||
|
|
||||||
private addHooks() {
|
private addHooks() {
|
||||||
// L.Draw.Feature.prototype.addHooks.call(this);
|
|
||||||
this._map = this.mapService.getMap(this.mapId);
|
this._map = this.mapService.getMap(this.mapId);
|
||||||
if (this._map) {
|
if (this._map) {
|
||||||
this._mapDraggable = this._map.dragging.enabled();
|
this._mapDraggable = this._map.dragging.enabled();
|
||||||
if (this._mapDraggable) {
|
if (this._mapDraggable) {
|
||||||
this._map.dragging.disable();
|
this._map.dragging.disable();
|
||||||
}
|
}
|
||||||
//TODO refactor: move cursor to styles
|
this._map.getContainer().style.cursor = 'crosshair';
|
||||||
// this._map.domElement.style.cursor = 'crosshair';
|
|
||||||
this._map._container.style.cursor = 'crosshair';
|
|
||||||
// this._tooltip.updateContent({text: this._initialLabelText});
|
|
||||||
|
|
||||||
this._map
|
this._map
|
||||||
.on('mousedown', this._onMouseDown, this)
|
.on('mousedown', this._onMouseDown, this)
|
||||||
.on('mousemove', this._onMouseMove, this)
|
.on('mousemove', this._onMouseMove, this)
|
||||||
.on('touchstart', this._onMouseDown, this)
|
.on('touchstart', this._onMouseDown, this)
|
||||||
.on('touchmove', this._onMouseMove, this);
|
.on('touchmove', this._onMouseMove, this);
|
||||||
// we should prevent default, otherwise default behavior (scrolling) will fire,
|
|
||||||
// and that will cause document.touchend to fire and will stop the drawing
|
|
||||||
// (circle, rectangle) in touch mode.
|
|
||||||
// (update): we have to send passive now to prevent scroll, because by default it is {passive: true} now, which means,
|
|
||||||
// handler can't event.preventDefault
|
|
||||||
// check the news https://developers.google.com/web/updates/2016/06/passive-event-listeners
|
|
||||||
// document.addEventListener('touchstart', preventDefault, { passive: false });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeHooks() {
|
private removeHooks() {
|
||||||
// L.Draw.Feature.prototype.removeHooks.call(this);
|
|
||||||
if (this._map) {
|
if (this._map) {
|
||||||
if (this._mapDraggable) {
|
if (this._mapDraggable) {
|
||||||
this._map.dragging.enable();
|
this._map.dragging.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO refactor: move cursor to styles
|
this._map.getContainer().style.cursor = '';
|
||||||
this._map._container.style.cursor = '';
|
|
||||||
|
|
||||||
this._map
|
this._map
|
||||||
.off('mousedown', this._onMouseDown, this)
|
.off('mousedown', this._onMouseDown, this)
|
||||||
|
|
@ -146,46 +148,36 @@ export default class DrawControlComponent extends Vue {
|
||||||
off(document, 'mouseup', this._onMouseUp, this);
|
off(document, 'mouseup', this._onMouseUp, this);
|
||||||
off(document, 'touchend', this._onMouseUp, this);
|
off(document, 'touchend', this._onMouseUp, this);
|
||||||
|
|
||||||
// document.removeEventListener('touchstart', preventDefault);
|
|
||||||
|
|
||||||
// If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return
|
|
||||||
if (this._shape && this.preserve == false) {
|
if (this._shape && this.preserve == false) {
|
||||||
this._map.removeLayer(this._shape);
|
this._map.removeLayer(this._shape);
|
||||||
// delete this._shape;
|
|
||||||
this._shape = undefined;
|
this._shape = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._isDrawing = false;
|
this._isDrawing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onMouseDown(e: LeafletMouseEvent) {
|
// private _onMouseDown(e: LeafletMouseEvent) {
|
||||||
|
private _onMouseDown(e: any) {
|
||||||
this._isDrawing = true;
|
this._isDrawing = true;
|
||||||
this._startLatLng = e.latlng;
|
this._startLatLng = e.latlng;
|
||||||
|
|
||||||
// DomEvent.on(document, 'mouseup', this._onMouseUp, this)
|
|
||||||
// .on(document, 'touchend', this._onMouseUp, this)
|
|
||||||
// .preventDefault(e.originalEvent);
|
|
||||||
on(document, 'mouseup', this._onMouseUp, this);
|
on(document, 'mouseup', this._onMouseUp, this);
|
||||||
on(document, 'touchend', this._onMouseUp, this);
|
on(document, 'touchend', this._onMouseUp, this);
|
||||||
preventDefault(e.originalEvent);
|
preventDefault(e.originalEvent);
|
||||||
}
|
}
|
||||||
|
// private _onMouseMove(e: LeafletMouseEvent) {
|
||||||
private _onMouseMove(e: LeafletMouseEvent) {
|
private _onMouseMove(e: any) {
|
||||||
var latlng = e.latlng;
|
var latlng = e.latlng;
|
||||||
|
|
||||||
// this._tooltip.updatePosition(latlng);
|
|
||||||
if (this._isDrawing) {
|
if (this._isDrawing) {
|
||||||
// this._tooltip.updateContent(this._getTooltipText());
|
|
||||||
this._drawShape(latlng);
|
this._drawShape(latlng);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onMouseUp() {
|
private _onMouseUp() {
|
||||||
if (this._shape) {
|
if (this._shape) {
|
||||||
this._fireCreatedEvent(this._shape);
|
this._fireCreatedEvent(this._shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.removeHooks();
|
|
||||||
this.disable();
|
this.disable();
|
||||||
if (this.options.repeatMode) {
|
if (this.options.repeatMode) {
|
||||||
this.enable();
|
this.enable();
|
||||||
|
|
@ -194,14 +186,12 @@ export default class DrawControlComponent extends Vue {
|
||||||
|
|
||||||
private _fireCreatedEvent(shape: Rectangle) {
|
private _fireCreatedEvent(shape: Rectangle) {
|
||||||
var rectangle = new Rectangle(shape.getBounds(), this.options.shapeOptions);
|
var rectangle = new Rectangle(shape.getBounds(), this.options.shapeOptions);
|
||||||
// L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle);
|
|
||||||
this._map.fire('Draw.Event.CREATED', { layer: rectangle, type: this.TYPE });
|
this._map.fire('Draw.Event.CREATED', { layer: rectangle, type: this.TYPE });
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeShape() {
|
public removeShape() {
|
||||||
if (this._shape) {
|
if (this._shape) {
|
||||||
this._map.removeLayer(this._shape);
|
this._map.removeLayer(this._shape);
|
||||||
// delete this._shape;
|
|
||||||
this._shape = undefined;
|
this._shape = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +200,6 @@ export default class DrawControlComponent extends Vue {
|
||||||
if (!this._shape) {
|
if (!this._shape) {
|
||||||
const bounds = new LatLngBounds(southWest, northEast);
|
const bounds = new LatLngBounds(southWest, northEast);
|
||||||
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
||||||
// this._map.addLayer(this._shape);
|
|
||||||
this._map = this.mapService.getMap(this.mapId);
|
this._map = this.mapService.getMap(this.mapId);
|
||||||
this._shape.addTo(this._map);
|
this._shape.addTo(this._map);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -218,12 +207,10 @@ export default class DrawControlComponent extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from Draw Rectangle
|
|
||||||
private _drawShape(latlng: LatLng) {
|
private _drawShape(latlng: LatLng) {
|
||||||
if (!this._shape) {
|
if (!this._shape) {
|
||||||
const bounds = new LatLngBounds(this._startLatLng, latlng);
|
const bounds = new LatLngBounds(this._startLatLng, latlng);
|
||||||
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
||||||
// this._map.addLayer(this._shape);
|
|
||||||
this._shape.addTo(this._map);
|
this._shape.addTo(this._map);
|
||||||
} else {
|
} else {
|
||||||
this._shape.setBounds(new LatLngBounds(this._startLatLng, latlng));
|
this._shape.setBounds(new LatLngBounds(this._startLatLng, latlng));
|
||||||
|
|
@ -237,44 +224,336 @@ export default class DrawControlComponent extends Vue {
|
||||||
this.enable();
|
this.enable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private enable() {
|
|
||||||
// //if (this.map.mapTool) this.map.mapTool.on('editable:drawing:start', this.disable.bind(this));
|
|
||||||
// // dom.addClass(this.map.container, 'measure-enabled');
|
|
||||||
// //this.fireAndForward('showmeasure');
|
|
||||||
// this._startMarker(this.southWest, this.options);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private disable() {
|
|
||||||
// //if (this.map.mapTool) this.map.mapTool.off('editable:drawing:start', this.disable.bind(this));
|
|
||||||
// // dom.removeClass(this.map.container, 'measure-enabled');
|
|
||||||
// // this.featuresLayer.clearLayers();
|
|
||||||
// // //this.fireAndForward('hidemeasure');
|
|
||||||
// // if (this._drawingEditor) {
|
|
||||||
// // this._drawingEditor.cancelDrawing();
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
export default DrawControlComponent;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css">
|
<style scoped>
|
||||||
.gba-control-draw {
|
.draw-control-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 8rem;
|
||||||
|
z-index: 1000;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 100px;
|
|
||||||
z-index: 40;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group-vertical button {
|
.draw-button {
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0.625rem;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
outline: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
margin-left: 0;
|
.dark .draw-button {
|
||||||
margin-top: 0.5em;
|
background: #1f2937;
|
||||||
|
border-color: #374151;
|
||||||
|
color: #d1d5db;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inactive state hover */
|
||||||
|
.draw-button:not(.is-active):hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-color: #65DC21;
|
||||||
|
color: #357C06;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
/* box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); */
|
||||||
|
width: auto;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .draw-button:not(.is-active):hover {
|
||||||
|
background: #111827;
|
||||||
|
border-color: #65DC21;
|
||||||
|
color: #65DC21;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state */
|
||||||
|
.draw-button.is-active {
|
||||||
|
background: linear-gradient(135deg, #65DC21 0%, #357C06 100%);
|
||||||
|
border-color: #357C06;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(101, 220, 33, 0.4), 0 4px 6px -2px rgba(101, 220, 33, 0.2);
|
||||||
|
width: auto;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .draw-button.is-active {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(101, 220, 33, 0.5), 0 4px 6px -2px rgba(101, 220, 33, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state hover */
|
||||||
|
.draw-button.is-active:hover {
|
||||||
|
background: linear-gradient(135deg, #429E04 0%, #295B09 100%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(101, 220, 33, 0.4), 0 10px 10px -5px rgba(101, 220, 33, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state press */
|
||||||
|
.draw-button:active {
|
||||||
|
transform: translateY(0) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus state */
|
||||||
|
.draw-button:focus-visible {
|
||||||
|
outline: 3px solid rgba(101, 220, 33, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon styling */
|
||||||
|
.draw-button :deep(svg) {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .draw-button.is-active :deep(svg) {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Status badge */
|
||||||
|
.draw-status-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show badge on hover when inactive */
|
||||||
|
.draw-button:not(.is-active):hover .draw-status-badge {
|
||||||
|
max-width: 100px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show badge when active */
|
||||||
|
.draw-button.is-active .draw-status-badge {
|
||||||
|
max-width: 100px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse animation for active state */
|
||||||
|
.draw-button.is-active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
animation: pulse 2s ease-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Glow effect for active state */
|
||||||
|
.draw-button.is-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
background: linear-gradient(135deg, #65DC21, #357C06);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button.is-active:hover::after {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inactive state indicator */
|
||||||
|
.draw-button:not(.is-active) .draw-status-badge {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .draw-button:not(.is-active) .draw-status-badge {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button:not(.is-active):hover .draw-status-badge {
|
||||||
|
color: #357C06;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .draw-button:not(.is-active):hover .draw-status-badge {
|
||||||
|
color: #65DC21;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state indicator */
|
||||||
|
.draw-button.is-active .draw-status-badge {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip on hover */
|
||||||
|
.draw-button:hover::after {
|
||||||
|
content: attr(aria-label);
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 0.5rem);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #1f2937;
|
||||||
|
color: white;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 1001;
|
||||||
|
animation: fadeInTooltip 0.2s ease 0.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @keyframes fadeInTooltip {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Ripple effect on click */
|
||||||
|
.draw-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.6s, height 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button:active::before {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.draw-control-container {
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button:not(.is-active):hover,
|
||||||
|
.draw-button.is-active {
|
||||||
|
padding: 0.5rem 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button :deep(svg) {
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-status-badge {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide tooltip on mobile */
|
||||||
|
.draw-button:hover::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @media (max-width: 640px) {
|
||||||
|
.draw-control-container {
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button:not(.is-active):hover,
|
||||||
|
.draw-button.is-active {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button :deep(svg) {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Accessibility: reduce motion */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.draw-button,
|
||||||
|
.draw-button :deep(svg),
|
||||||
|
.draw-status-badge {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-button.is-active::before,
|
||||||
|
.draw-button.is-active::after {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Global styles for draw mode */
|
||||||
|
.leaflet-container.draw-mode-active {
|
||||||
|
cursor: crosshair !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.draw-mode-active * {
|
||||||
|
cursor: crosshair !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,21 +1,72 @@
|
||||||
<template>
|
<template>
|
||||||
<div style="position: relative">
|
<div class="relative w-full">
|
||||||
<!-- <Map className="h-36" :center="state.center" :zoom="state.zoom"> // map component content </Map> -->
|
<!-- Map Container -->
|
||||||
<div :id="mapId" class="rounded">
|
<div
|
||||||
<div class="dark:bg-slate-900 bg-slate flex flex-col">
|
:id="mapId"
|
||||||
|
class="relative h-[600px] w-full bg-gray-50 dark:bg-gray-900 rounded-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<div class="relative w-full h-full">
|
||||||
<ZoomControlComponent ref="zoom" :mapId="mapId" />
|
<ZoomControlComponent ref="zoom" :mapId="mapId" />
|
||||||
<DrawControlComponent ref="draw" :mapId="mapId" :southWest="southWest" :northEast="northEast" />
|
<DrawControlComponent ref="draw" :mapId="mapId" :southWest="southWest" :northEast="northEast" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gba-control-validate btn-group-vertical">
|
|
||||||
|
<!-- Validate Button -->
|
||||||
|
<div class="absolute left-4 top-44 z-[1000] select-none">
|
||||||
<button
|
<button
|
||||||
class="min-w-27 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black text-sm p-1"
|
class="group flex items-center justify-center relative overflow-visible outline-none font-semibold text-sm transition-all duration-300 ease-in-out
|
||||||
|
w-10 h-10 rounded-xl border-2 shadow-md
|
||||||
|
focus-visible:outline focus-visible:outline-3 focus-visible:outline-offset-2"
|
||||||
|
:class="[
|
||||||
|
validBoundingBox
|
||||||
|
? 'bg-gradient-to-br from-lime-500 to-lime-700 border-lime-700 text-white shadow-lime-500/40 cursor-default gap-2 w-auto px-4 focus-visible:outline-lime-500/50'
|
||||||
|
: 'bg-white dark:bg-gray-800 border-red-500 text-red-600 dark:text-red-400 gap-0 hover:bg-red-50 dark:hover:bg-gray-900 hover:border-red-500 hover:text-red-700 dark:hover:text-red-300 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-red-500/30 hover:w-auto hover:px-4 hover:gap-2 focus-visible:outline-red-500/50'
|
||||||
|
]"
|
||||||
type="button"
|
type="button"
|
||||||
@click.stop.prevent="validateBoundingBox"
|
@click.stop.prevent="validateBoundingBox"
|
||||||
:class="[validBoundingBox ? 'cursor-not-allowed bg-green-500 is-active' : 'bg-red-500 ']"
|
:aria-label="validBoundingBox ? 'Bounding box is valid' : 'Validate bounding box'"
|
||||||
>
|
>
|
||||||
<!-- <BaseIcon v-if="mdiMapCheckOutline" :path="mdiMapCheckOutline" /> -->
|
<!-- Icon -->
|
||||||
{{ label }}
|
<BaseIcon
|
||||||
|
v-if="mdiMapCheckOutline"
|
||||||
|
:path="mdiMapCheckOutline"
|
||||||
|
:size="20"
|
||||||
|
:class="[
|
||||||
|
'transition-transform duration-300',
|
||||||
|
validBoundingBox && 'animate-[checkPulse_2s_ease-in-out_infinite]'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Status badge -->
|
||||||
|
<span
|
||||||
|
class="text-xs font-bold uppercase tracking-wider whitespace-nowrap transition-all duration-300 overflow-hidden"
|
||||||
|
:class="[
|
||||||
|
validBoundingBox
|
||||||
|
? 'max-w-[100px] opacity-100 text-white drop-shadow'
|
||||||
|
: 'max-w-0 opacity-0 group-hover:max-w-[100px] group-hover:opacity-100'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Pulse animation for valid state -->
|
||||||
|
<span
|
||||||
|
v-if="validBoundingBox"
|
||||||
|
class="absolute top-1/2 left-1/2 w-full h-full bg-white/30 rounded-xl -translate-x-1/2 -translate-y-1/2 animate-[pulse_2s_ease-out_infinite] pointer-events-none"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<!-- Ripple effect on click -->
|
||||||
|
<span
|
||||||
|
class="absolute top-1/2 left-1/2 w-0 h-0 rounded-full bg-white/30 -translate-x-1/2 -translate-y-1/2 transition-all duration-[600ms] active:w-[300px] active:h-[300px]"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<span
|
||||||
|
v-if="!validBoundingBox"
|
||||||
|
class="absolute left-[calc(100%+0.5rem)] top-1/2 -translate-y-1/2 px-3 py-1.5 bg-gray-800 text-white text-xs rounded-md whitespace-nowrap opacity-0 pointer-events-none transition-opacity duration-200 z-[1001] group-hover:opacity-100 group-hover:animate-[fadeInTooltip_0.2s_ease_0.5s_forwards]"
|
||||||
|
>
|
||||||
|
Click to validate
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -27,8 +78,7 @@ import { Component, Vue, Prop, Ref } from 'vue-facing-decorator';
|
||||||
import { Map } from 'leaflet/src/map/index';
|
import { Map } from 'leaflet/src/map/index';
|
||||||
import { Control } from 'leaflet/src/control/Control';
|
import { Control } from 'leaflet/src/control/Control';
|
||||||
import { LatLngBoundsExpression, LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
import { LatLngBoundsExpression, LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||||
// import { toLatLng } from 'leaflet/src/geo/LatLng';
|
import { LatLng } from 'leaflet';
|
||||||
import { LatLng } from 'leaflet'; //'leaflet/src/geo/LatLng';
|
|
||||||
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
||||||
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
||||||
import { mdiMapCheckOutline } from '@mdi/js';
|
import { mdiMapCheckOutline } from '@mdi/js';
|
||||||
|
|
@ -37,22 +87,15 @@ import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import { MapOptions } from './MapOptions';
|
import { MapOptions } from './MapOptions';
|
||||||
import { LayerOptions, LayerMap } from './LayerOptions';
|
import { LayerOptions, LayerMap } from './LayerOptions';
|
||||||
import { MapService } from '@/Stores/map.service';
|
import { MapService } from '@/Stores/map.service';
|
||||||
import ZoomControlComponent from './zoom.component.vue';
|
import { ZoomControlComponent } from './zoom.component.vue';
|
||||||
import DrawControlComponent from './draw.component.vue';
|
import { DrawControlComponent } from './draw.component.vue';
|
||||||
import { Coverage } from '@/Dataset';
|
import { Coverage } from '@/Dataset';
|
||||||
import { canvas } from 'leaflet/src/layer/vector/Canvas';
|
import { canvas } from 'leaflet/src/layer/vector/Canvas';
|
||||||
import { svg } from 'leaflet/src/layer/vector/SVG';
|
import { svg } from 'leaflet/src/layer/vector/SVG';
|
||||||
import Notification from '@/utils/toast';
|
import Notification from '@/utils/toast';
|
||||||
|
|
||||||
Map.include({
|
Map.include({
|
||||||
// @namespace Map; @method getRenderer(layer: Path): Renderer
|
|
||||||
// Returns the instance of `Renderer` that should be used to render the given
|
|
||||||
// `Path`. It will ensure that the `renderer` options of the map and paths
|
|
||||||
// are respected, and that the renderers do exist on the map.
|
|
||||||
getRenderer: function (layer) {
|
getRenderer: function (layer) {
|
||||||
// @namespace Path; @option renderer: Renderer
|
|
||||||
// Use this specific instance of `Renderer` for this path. Takes
|
|
||||||
// precedence over the map's [default renderer](#map-renderer).
|
|
||||||
var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
|
var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
|
||||||
|
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
|
|
@ -79,15 +122,11 @@ Map.include({
|
||||||
},
|
},
|
||||||
|
|
||||||
_createRenderer: function (options) {
|
_createRenderer: function (options) {
|
||||||
// @namespace Map; @option preferCanvas: Boolean = false
|
|
||||||
// Whether `Path`s should be rendered on a `Canvas` renderer.
|
|
||||||
// By default, all `Path`s are rendered in a `SVG` renderer.
|
|
||||||
return (this.options.preferCanvas && canvas(options)) || svg(options);
|
return (this.options.preferCanvas && canvas(options)) || svg(options);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
|
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
|
||||||
// const DEFAULT_BASE_LAYER_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
|
||||||
const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -98,33 +137,19 @@ const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://o
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class MapComponent extends Vue {
|
export class MapComponent extends Vue {
|
||||||
/**
|
|
||||||
* A map with the given ID is created inside this component.
|
|
||||||
* This ID can be used the get the map instance over the map cache service.
|
|
||||||
*/
|
|
||||||
@Prop()
|
@Prop()
|
||||||
public mapId: string;
|
public mapId: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The corresponding leaflet map options (see: https://leafletjs.com/reference-1.3.4.html#map-option)
|
|
||||||
*/
|
|
||||||
@Prop()
|
@Prop()
|
||||||
public mapOptions: MapOptions;
|
public mapOptions: MapOptions;
|
||||||
|
|
||||||
@Prop()
|
@Prop()
|
||||||
public coverage: Coverage;
|
public coverage: Coverage;
|
||||||
|
|
||||||
// markerService: MarkerService
|
|
||||||
/**
|
|
||||||
* Bounds for the map
|
|
||||||
*/
|
|
||||||
@Prop({ default: null })
|
@Prop({ default: null })
|
||||||
public fitBounds: LatLngBoundsExpression;
|
public fitBounds: LatLngBoundsExpression;
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes the the zoom control options (see: https://leafletjs.com/reference-1.3.4.html#control-zoom)
|
|
||||||
*/
|
|
||||||
@Prop()
|
@Prop()
|
||||||
public zoomControlOptions: Control.ZoomOptions;
|
public zoomControlOptions: Control.ZoomOptions;
|
||||||
|
|
||||||
|
|
@ -132,7 +157,7 @@ export default class MapComponent extends Vue {
|
||||||
public baseMaps: LayerMap;
|
public baseMaps: LayerMap;
|
||||||
|
|
||||||
get label(): string {
|
get label(): string {
|
||||||
return this.validBoundingBox ? ' valid' : 'invalid';
|
return this.validBoundingBox ? 'Valid' : 'Invalid';
|
||||||
}
|
}
|
||||||
|
|
||||||
get validBoundingBox(): boolean {
|
get validBoundingBox(): boolean {
|
||||||
|
|
@ -144,35 +169,31 @@ export default class MapComponent extends Vue {
|
||||||
|
|
||||||
let isBoundValid = true;
|
let isBoundValid = true;
|
||||||
if (isValidNumber) {
|
if (isValidNumber) {
|
||||||
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
let _southWest: LatLng = new LatLng(this.coverage.y_min!, this.coverage.x_min!);
|
||||||
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
let _northEast: LatLng = new LatLng(this.coverage.y_max!, this.coverage.x_max!);
|
||||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||||
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||||
// this.draw.removeShape();
|
|
||||||
// Notification.showTemporary('Bounds are not valid.');
|
|
||||||
isBoundValid = false;
|
isBoundValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isValidNumber && isBoundValid;
|
return isValidNumber && isBoundValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ref('zoom') private zoom: ZoomControlComponent;
|
@Ref('zoom')
|
||||||
@Ref('draw') private draw: DrawControlComponent;
|
private zoom: ZoomControlComponent;
|
||||||
|
|
||||||
|
@Ref('draw')
|
||||||
|
private draw: DrawControlComponent;
|
||||||
|
|
||||||
// services:
|
|
||||||
mapService = MapService();
|
mapService = MapService();
|
||||||
|
|
||||||
mdiMapCheckOutline = mdiMapCheckOutline;
|
mdiMapCheckOutline = mdiMapCheckOutline;
|
||||||
southWest: LatLng;
|
southWest: LatLng;
|
||||||
northEast: LatLng;
|
northEast: LatLng;
|
||||||
|
|
||||||
/**
|
|
||||||
* Informs when initialization is done with map id.
|
|
||||||
*/
|
|
||||||
public onMapInitializedEvent: EventEmitter<string> = new EventEmitter<string>();
|
public onMapInitializedEvent: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
|
||||||
public map!: Map;
|
public map!: Map;
|
||||||
// protected drawnItems!: FeatureGroup<any>;
|
|
||||||
|
|
||||||
validateBoundingBox() {
|
validateBoundingBox() {
|
||||||
if (this.validBoundingBox == false) {
|
if (this.validBoundingBox == false) {
|
||||||
|
|
@ -182,53 +203,22 @@ export default class MapComponent extends Vue {
|
||||||
}
|
}
|
||||||
this.map.control && this.map.control.disable();
|
this.map.control && this.map.control.disable();
|
||||||
var _this = this;
|
var _this = this;
|
||||||
// // _this.locationErrors.length = 0;
|
|
||||||
// this.drawnItems.clearLayers();
|
|
||||||
// //var xmin = document.getElementById("xmin").value;
|
|
||||||
// var xmin = (<HTMLInputElement>document.getElementById("xmin")).value;
|
|
||||||
// // var ymin = document.getElementById("ymin").value;
|
|
||||||
// var ymin = (<HTMLInputElement>document.getElementById("ymin")).value;
|
|
||||||
// //var xmax = document.getElementById("xmax").value;
|
|
||||||
// var xmax = (<HTMLInputElement>document.getElementById("xmax")).value;
|
|
||||||
// //var ymax = document.getElementById("ymax").value;
|
|
||||||
// var ymax = (<HTMLInputElement>document.getElementById("ymax")).value;
|
|
||||||
// var bounds = [[ymin, xmin], [ymax, xmax]];
|
|
||||||
|
|
||||||
// let _southWest: LatLng;
|
let _southWest: LatLng = new LatLng(this.coverage.y_min!, this.coverage.x_min!);
|
||||||
// let _northEast: LatLng;
|
let _northEast: LatLng = new LatLng(this.coverage.y_max!, this.coverage.x_max!);
|
||||||
// if (this.coverage.x_min && this.coverage.y_min) {
|
|
||||||
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
|
||||||
// }
|
|
||||||
// if (this.coverage.x_max && this.coverage.y_max) {
|
|
||||||
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
|
||||||
// }
|
|
||||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||||
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||||
this.draw.removeShape();
|
this.draw.removeShape();
|
||||||
Notification.showTemporary('Bounds are not valid.');
|
Notification.showTemporary('Bounds are not valid.');
|
||||||
} else {
|
} else {
|
||||||
// this.draw.drawShape(_southWest, _northEast);
|
|
||||||
try {
|
try {
|
||||||
this.draw.drawShape(_southWest, _northEast);
|
this.draw.drawShape(_southWest, _northEast);
|
||||||
_this.map.fitBounds(bounds);
|
_this.map.fitBounds(bounds);
|
||||||
|
|
||||||
// var boundingBox = L.rectangle(bounds, { color: "#005F6A", weight: 1 });
|
Notification.showSuccess('Valid bounding box');
|
||||||
// // this.geolocation.xmin = xmin;
|
|
||||||
// // this.geolocation.ymin = ymin;
|
|
||||||
// // this.geolocation.xmax = xmax;
|
|
||||||
// // this.geolocation.ymax = ymax;
|
|
||||||
|
|
||||||
// _this.drawnItems.addLayer(boundingBox);
|
|
||||||
// _this.map.fitBounds(bounds);
|
|
||||||
// this.options.message = "valid bounding box";
|
|
||||||
// this.$toast.success("valid bounding box", this.options);
|
|
||||||
Notification.showSuccess('valid bounding box');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// this.options.message = e.message;
|
|
||||||
// // _this.errors.push(e);
|
|
||||||
// this.$toast.error(e.message, this.options);
|
|
||||||
Notification.showTemporary('An error occurred while drawing bounding box');
|
Notification.showTemporary('An error occurred while drawing bounding box');
|
||||||
// generatingCodes.value = false;
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,16 +232,11 @@ export default class MapComponent extends Vue {
|
||||||
this.map.off('zoomend zoomlevelschange');
|
this.map.off('zoomend zoomlevelschange');
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Emit(this.onMapInitializedEvent)
|
|
||||||
protected initMap(): void {
|
protected initMap(): void {
|
||||||
// let map: Map = (this.map = this.mapService.getMap(this.mapId));
|
|
||||||
|
|
||||||
let map: Map = (this.map = new Map(this.mapId, this.mapOptions));
|
let map: Map = (this.map = new Map(this.mapId, this.mapOptions));
|
||||||
this.mapService.setMap(this.mapId, map);
|
this.mapService.setMap(this.mapId, map);
|
||||||
map.scrollWheelZoom.disable();
|
map.scrollWheelZoom.disable();
|
||||||
|
|
||||||
// return this.mapId;
|
|
||||||
// this.$emit("onMapInitializedEvent", this.mapId);
|
|
||||||
this.onMapInitializedEvent.emit(this.mapId);
|
this.onMapInitializedEvent.emit(this.mapId);
|
||||||
this.addBaseMap();
|
this.addBaseMap();
|
||||||
|
|
||||||
|
|
@ -260,45 +245,28 @@ export default class MapComponent extends Vue {
|
||||||
|
|
||||||
map.on(
|
map.on(
|
||||||
'Draw.Event.CREATED',
|
'Draw.Event.CREATED',
|
||||||
function (event) {
|
(event: any) => {
|
||||||
// drawnItems.clearLayers();
|
|
||||||
// var type = event.type;
|
|
||||||
var layer = event.layer;
|
var layer = event.layer;
|
||||||
|
|
||||||
// if (type === "rectancle") {
|
|
||||||
// layer.bindPopup("A popup!" + layer.getBounds().toBBoxString());
|
|
||||||
var bounds = layer.getBounds();
|
var bounds = layer.getBounds();
|
||||||
this.coverage.x_min = bounds.getSouthWest().lng;
|
this.coverage.x_min = bounds.getSouthWest().lng;
|
||||||
this.coverage.y_min = bounds.getSouthWest().lat;
|
this.coverage.y_min = bounds.getSouthWest().lat;
|
||||||
// console.log(this.geolocation.xmin);
|
|
||||||
this.coverage.x_max = bounds.getNorthEast().lng;
|
this.coverage.x_max = bounds.getNorthEast().lng;
|
||||||
this.coverage.y_max = bounds.getNorthEast().lat;
|
this.coverage.y_max = bounds.getNorthEast().lat;
|
||||||
// }
|
|
||||||
|
|
||||||
// drawnItems.addLayer(layer);
|
|
||||||
},
|
},
|
||||||
this,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialise the FeatureGroup to store editable layers
|
|
||||||
// let drawnItems = (this.drawnItems = new FeatureGroup());
|
|
||||||
// map.addLayer(drawnItems);
|
|
||||||
|
|
||||||
this.map.on('zoomend zoomlevelschange', this.zoom.updateDisabled, this.zoom);
|
this.map.on('zoomend zoomlevelschange', this.zoom.updateDisabled, this.zoom);
|
||||||
|
|
||||||
// if (this.fitBounds) {
|
|
||||||
// this.map.fitBounds(this.fitBounds);
|
|
||||||
// }
|
|
||||||
if (this.coverage.x_min && this.coverage.y_min) {
|
if (this.coverage.x_min && this.coverage.y_min) {
|
||||||
this.southWest = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
this.southWest = new LatLng(this.coverage.y_min!, this.coverage.x_min!);
|
||||||
} else {
|
} else {
|
||||||
this.southWest = new LatLng(46.5, 9.9);
|
this.southWest = new LatLng(46.5, 9.9);
|
||||||
}
|
}
|
||||||
if (this.coverage.x_max && this.coverage.y_max) {
|
if (this.coverage.x_max && this.coverage.y_max) {
|
||||||
this.northEast = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
this.northEast = new LatLng(this.coverage.y_max!, this.coverage.x_max!);
|
||||||
} else {
|
} else {
|
||||||
this.northEast = new LatLng(48.9, 16.9);
|
this.northEast = new LatLng(48.9, 16.9);
|
||||||
} // this.northEast = toLatLng(48.9, 16.9);
|
}
|
||||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||||
map.fitBounds(bounds);
|
map.fitBounds(bounds);
|
||||||
|
|
||||||
|
|
@ -318,10 +286,6 @@ export default class MapComponent extends Vue {
|
||||||
private addBaseMap(layerOptions?: LayerOptions): void {
|
private addBaseMap(layerOptions?: LayerOptions): void {
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
if (!this.baseMaps || this.baseMaps.size === 0) {
|
if (!this.baseMaps || this.baseMaps.size === 0) {
|
||||||
// let bmapgrau = tileLayer('https://{s}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png', {
|
|
||||||
// subdomains: ['maps', 'maps1', 'maps2', 'maps3', 'maps4'],
|
|
||||||
// attribution: 'Datenquelle: <a href="http://www.basemap.at/">basemap.at</a>',
|
|
||||||
// });
|
|
||||||
let osmGgray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
|
let osmGgray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
|
||||||
format: 'image/png',
|
format: 'image/png',
|
||||||
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
|
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
|
||||||
|
|
@ -337,45 +301,61 @@ export default class MapComponent extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export default MapComponent;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped>
|
||||||
/* .leaflet-container {
|
/* Leaflet container - only what can't be done with Tailwind */
|
||||||
|
:deep(.leaflet-container) {
|
||||||
height: 600px;
|
height: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: transparent;
|
background: transparent;
|
||||||
outline-offset: 1px;
|
|
||||||
} */
|
|
||||||
.leaflet-container {
|
|
||||||
height: 600px;
|
|
||||||
width: 100%;
|
|
||||||
background: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gba-control-validate {
|
:deep(.leaflet-container .leaflet-pane) {
|
||||||
-webkit-user-select: none;
|
z-index: 30 !important;
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 150px;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group-vertical button {
|
/* Custom animations */
|
||||||
display: block;
|
@keyframes checkPulse {
|
||||||
|
0%, 100% {
|
||||||
margin-left: 0;
|
transform: scale(1);
|
||||||
margin-top: 0.5em;
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-container .leaflet-pane {
|
@keyframes pulse {
|
||||||
z-index: 30!important;
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInTooltip {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.leaflet-container) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
:deep(.leaflet-container) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* .leaflet-pane {
|
|
||||||
z-index: 30;
|
|
||||||
} */
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="gba-control-zoom btn-group-vertical">
|
<div class="zoom-control-container">
|
||||||
<button
|
<button
|
||||||
ref="inputPlus"
|
ref="inputPlus"
|
||||||
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
class="zoom-button zoom-button-plus"
|
||||||
type="button"
|
type="button"
|
||||||
@click.stop.prevent="zoomIn"
|
@click.stop.prevent="zoomIn"
|
||||||
|
:disabled="isZoomInDisabled"
|
||||||
|
aria-label="Zoom in"
|
||||||
>
|
>
|
||||||
<BaseIcon v-if="mdiPlus" :path="mdiPlus" />
|
<BaseIcon v-if="mdiPlus" :path="mdiPlus" :size="20" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="zoom-separator"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
ref="inputMinus"
|
ref="inputMinus"
|
||||||
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
class="zoom-button zoom-button-minus"
|
||||||
type="button"
|
type="button"
|
||||||
@click.stop.prevent="zoomOut"
|
@click.stop.prevent="zoomOut"
|
||||||
|
:disabled="isZoomOutDisabled"
|
||||||
|
aria-label="Zoom out"
|
||||||
>
|
>
|
||||||
<BaseIcon v-if="mdiMinus" :path="mdiMinus" />
|
<BaseIcon v-if="mdiMinus" :path="mdiMinus" :size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -26,6 +32,7 @@ import { MapService } from '@/Stores/map.service';
|
||||||
|
|
||||||
import BaseIcon from '@/Components/BaseIcon.vue';
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import { mdiPlus, mdiMinus } from '@mdi/js';
|
import { mdiPlus, mdiMinus } from '@mdi/js';
|
||||||
|
import { Map } from 'leaflet';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'zoom-control',
|
name: 'zoom-control',
|
||||||
|
|
@ -33,7 +40,7 @@ import { mdiPlus, mdiMinus } from '@mdi/js';
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ZoomControlComponent extends Vue {
|
export class ZoomControlComponent extends Vue {
|
||||||
mdiPlus = mdiPlus;
|
mdiPlus = mdiPlus;
|
||||||
mdiMinus = mdiMinus;
|
mdiMinus = mdiMinus;
|
||||||
|
|
||||||
|
|
@ -46,16 +53,23 @@ export default class ZoomControlComponent extends Vue {
|
||||||
@Ref('inputMinus') inputMinus: HTMLButtonElement;
|
@Ref('inputMinus') inputMinus: HTMLButtonElement;
|
||||||
|
|
||||||
mapService = MapService();
|
mapService = MapService();
|
||||||
map;
|
map: Map | null = null;
|
||||||
|
isZoomInDisabled = false;
|
||||||
|
isZoomOutDisabled = false;
|
||||||
|
|
||||||
// mounted() {
|
mounted() {
|
||||||
// let map = (this.map = this.mapService.getMap(this.mapId));
|
let map = (this.map = this.mapService.getMap(this.mapId));
|
||||||
// map.on('zoomend zoomlevelschange', this.updateDisabled, this);
|
if (map) {
|
||||||
// }
|
map.on('zoomend zoomlevelschange', this.updateDisabled, this);
|
||||||
|
this.updateDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// unmounted() {
|
unmounted() {
|
||||||
// this.map.off('zoomend zoomlevelschange');
|
if (this.map) {
|
||||||
// }
|
this.map.off('zoomend zoomlevelschange', this.updateDisabled, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public zoomIn() {
|
public zoomIn() {
|
||||||
let map = this.mapService.getMap(this.mapId);
|
let map = this.mapService.getMap(this.mapId);
|
||||||
|
|
@ -69,44 +83,266 @@ export default class ZoomControlComponent extends Vue {
|
||||||
|
|
||||||
public updateDisabled() {
|
public updateDisabled() {
|
||||||
let map = this.mapService.getMap(this.mapId);
|
let map = this.mapService.getMap(this.mapId);
|
||||||
// let className = 'leaflet-disabled';
|
if (!map) return;
|
||||||
|
|
||||||
this.inputPlus.disabled = false;
|
this.isZoomInDisabled = map.getZoom() >= map.getMaxZoom();
|
||||||
this.inputPlus.setAttribute('aria-disabled', 'false');
|
this.isZoomOutDisabled = map.getZoom() <= map.getMinZoom();
|
||||||
|
|
||||||
this.inputMinus.disabled = false;
|
if (this.inputPlus) {
|
||||||
this.inputMinus.setAttribute('aria-disabled', 'false');
|
this.inputPlus.disabled = this.isZoomInDisabled;
|
||||||
|
this.inputPlus.setAttribute('aria-disabled', this.isZoomInDisabled.toString());
|
||||||
if (map.getZoom() === map.getMinZoom()) {
|
|
||||||
this.inputMinus.disabled = true;
|
|
||||||
this.inputMinus.setAttribute('aria-disabled', 'true');
|
|
||||||
}
|
}
|
||||||
if (map.getZoom() === map.getMaxZoom()) {
|
|
||||||
this.inputPlus.disabled = true;
|
if (this.inputMinus) {
|
||||||
this.inputPlus.setAttribute('aria-disabled', 'true');
|
this.inputMinus.disabled = this.isZoomOutDisabled;
|
||||||
|
this.inputMinus.setAttribute('aria-disabled', this.isZoomOutDisabled.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export default ZoomControlComponent;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css">
|
<style scoped>
|
||||||
.gba-control-zoom {
|
.zoom-control-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
transition: box-shadow 0.2s ease;
|
||||||
border-radius: 4px;
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 10px;
|
|
||||||
z-index: 40;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group-vertical button {
|
.zoom-control-container:hover {
|
||||||
display: block;
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
margin-left: 0;
|
.dark .zoom-control-container {
|
||||||
margin-top: 0.5em;
|
background: #1f2937;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .zoom-control-container:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .zoom-button {
|
||||||
|
background: #1f2937;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button:hover:not(:disabled) {
|
||||||
|
background: #65dc21;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .zoom-button:hover:not(:disabled) {
|
||||||
|
background: #65dc21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button:active:not(:disabled) {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .zoom-button:disabled {
|
||||||
|
background: #111827;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button:focus-visible {
|
||||||
|
outline: 2px solid #65dc21;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon sizing */
|
||||||
|
.zoom-button :deep(svg) {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separator between buttons */
|
||||||
|
.zoom-separator {
|
||||||
|
height: 1px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .zoom-separator {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for the plus button */
|
||||||
|
.zoom-button-plus::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(101, 220, 33, 0.1) 0%, rgba(53, 124, 6, 0.1) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button-plus:hover:not(:disabled)::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for the minus button */
|
||||||
|
.zoom-button-minus::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(101, 220, 33, 0.1) 0%, rgba(53, 124, 6, 0.1) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button-minus:hover:not(:disabled)::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.zoom-control-container {
|
||||||
|
left: 0.75rem;
|
||||||
|
top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button :deep(svg) {
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.zoom-control-container {
|
||||||
|
left: 0.5rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button :deep(svg) {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for button press */
|
||||||
|
@keyframes buttonPress {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button:active:not(:disabled) {
|
||||||
|
animation: buttonPress 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip-like effect on hover (optional) */
|
||||||
|
.zoom-button-plus:hover:not(:disabled)::before {
|
||||||
|
content: 'Zoom In';
|
||||||
|
position: absolute;
|
||||||
|
left: calc(100% + 0.5rem);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #1f2937;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.2s ease 0.5s forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-button-minus:hover:not(:disabled)::before {
|
||||||
|
content: 'Zoom Out';
|
||||||
|
position: absolute;
|
||||||
|
left: calc(100% + 0.5rem);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #1f2937;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.2s ease 0.5s forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide tooltips on mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.zoom-button-plus:hover:not(:disabled)::before,
|
||||||
|
.zoom-button-minus:hover:not(:disabled)::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const submit = async () => {
|
||||||
|
|
||||||
<CardBox form @submit.prevent="submit()" class="shadow-lg">
|
<CardBox form @submit.prevent="submit()" class="shadow-lg">
|
||||||
<div class="grid grid-cols-1 gap-6">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<FormField label="Label" help="Required. Displayed project label" :class="{ 'text-red-400': form.errors.label }">
|
<FormField label="Label" help="Lowercase letters, numbers, and hyphens only" :class="{ 'text-red-400': form.errors.label }">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -56,7 +56,7 @@ const submit = async () => {
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
help="Required. Project identifier (slug, lowercase, no spaces)"
|
help="Required. Project title shown to users"
|
||||||
:class="{ 'text-red-400': form.errors.name }"
|
:class="{ 'text-red-400': form.errors.name }"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,12 @@ const submit = async () => {
|
||||||
<div class="grid grid-cols-1 gap-6">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<FormField
|
<FormField
|
||||||
label="Label"
|
label="Label"
|
||||||
help="Project label (read-only)"
|
help="Lowercase letters, numbers, and hyphens only"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
type="text"
|
type="text"
|
||||||
|
help="Lowercase letters, numbers, and hyphens only"
|
||||||
:is-read-only=true
|
:is-read-only=true
|
||||||
class="bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
|
class="bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
|
||||||
/>
|
/>
|
||||||
|
|
@ -60,7 +61,7 @@ const submit = async () => {
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
help="Required. Project identifier (slug)"
|
help="Required. Project title shown to users"
|
||||||
:class="{ 'text-red-400': form.errors.name }"
|
:class="{ 'text-red-400': form.errors.name }"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves } from '@mdi/js';
|
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves, mdiAccountArrowLeft, mdiAccountArrowRight, mdiFingerprint, mdiPublish, mdiChevronDown, mdiChevronUp, mdiTrayArrowDown, mdiCheckDecagram } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
|
|
@ -26,78 +27,93 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
// user: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flash: ComputedRef<any> = computed(() => {
|
const flash: ComputedRef<any> = computed(() => {
|
||||||
// let test = usePage();
|
|
||||||
// console.log(test);
|
|
||||||
return usePage().props.flash;
|
return usePage().props.flash;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legend visibility state with localStorage persistence
|
||||||
|
const showLegend = ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedState = localStorage.getItem('datasetLegendVisible');
|
||||||
|
if (savedState !== null) {
|
||||||
|
showLegend.value = savedState === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLegend = () => {
|
||||||
|
showLegend.value = !showLegend.value;
|
||||||
|
localStorage.setItem('datasetLegendVisible', String(showLegend.value));
|
||||||
|
};
|
||||||
|
|
||||||
// const getRowClass = (dataset) => {
|
|
||||||
// // (props.options ? 'select' : props.type)
|
|
||||||
// let rowclass = '';
|
|
||||||
// if (dataset.server_state == 'accepted') {
|
|
||||||
// rowclass = 'bg-accepted';
|
|
||||||
// } else if (dataset.server_state == 'rejected_reviewer') {
|
|
||||||
// rowclass = 'bg-rejected-reviewer';
|
|
||||||
// } else if (dataset.server_state == 'reviewed') {
|
|
||||||
// rowclass = 'bg-reviewed';
|
|
||||||
// } else if (dataset.server_state == 'released') {
|
|
||||||
// rowclass = 'bg-released';
|
|
||||||
// } else if (dataset.server_state == 'published') {
|
|
||||||
// rowclass = 'bg-published';
|
|
||||||
// } else {
|
|
||||||
// rowclass = '';
|
|
||||||
// }
|
|
||||||
// return rowclass;
|
|
||||||
// };
|
|
||||||
const getRowClass = (dataset) => {
|
const getRowClass = (dataset) => {
|
||||||
// (props.options ? 'select' : props.type)
|
// Return Tailwind classes that will be defined in tailwind.config
|
||||||
let rowclass = '';
|
const stateClasses = {
|
||||||
if (dataset.server_state == 'released') {
|
'released': 'bg-released dark:bg-released-dark',
|
||||||
rowclass = 'bg-released';
|
'editor_accepted': 'bg-editor-accepted dark:bg-editor-accepted-dark',
|
||||||
} else if (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer') {
|
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
rowclass = 'bg-editor-accepted';
|
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
|
||||||
} else if (dataset.server_state == 'reviewed') {
|
'published': 'bg-published dark:bg-published-dark',
|
||||||
rowclass = 'bg-reviewed';
|
};
|
||||||
} else if (dataset.server_state == 'published') {
|
|
||||||
rowclass = 'bg-published';
|
|
||||||
} else {
|
|
||||||
rowclass = '';
|
|
||||||
}
|
|
||||||
return rowclass;
|
|
||||||
};
|
|
||||||
|
|
||||||
// New method to format server state
|
return stateClasses[dataset.server_state] || '';
|
||||||
const formatServerState = (state: string) => {
|
|
||||||
if (state === 'inprogress') {
|
|
||||||
return 'draft';
|
|
||||||
} else if (state === 'released') {
|
|
||||||
return 'submitted';
|
|
||||||
} else if (state === 'approved') {
|
|
||||||
return 'ready for review';
|
|
||||||
} else if (state === 'reviewer_accepted') {
|
|
||||||
return 'in review';
|
|
||||||
}
|
|
||||||
return state; // Return the original state for other cases
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Method to get state badge color
|
||||||
|
const getStateColor = (state: string) => {
|
||||||
|
const stateColors = {
|
||||||
|
'inprogress': 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200',
|
||||||
|
'released': 'bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-200',
|
||||||
|
'editor_accepted': 'bg-teal-200 text-teal-900 dark:bg-teal-900 dark:text-teal-200',
|
||||||
|
'rejected_reviewer': 'bg-amber-200 text-amber-900 dark:bg-amber-900 dark:text-amber-200',
|
||||||
|
'rejected_to_reviewer': 'bg-yellow-200 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
'rejected_editor': 'bg-rose-300 text-rose-900 dark:bg-rose-900 dark:text-rose-200',
|
||||||
|
'reviewed': 'bg-yellow-300 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
'published': 'bg-green-300 text-green-900 dark:bg-green-900 dark:text-green-200',
|
||||||
|
'approved': 'bg-cyan-200 text-cyan-900 dark:bg-cyan-900 dark:text-cyan-200',
|
||||||
|
'reviewer_accepted': 'bg-lime-200 text-lime-900 dark:bg-lime-900 dark:text-lime-200',
|
||||||
|
};
|
||||||
|
return stateColors[state] || 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamic legend definitions
|
||||||
|
const datasetStates = [
|
||||||
|
{ key: 'released', label: 'Submitted' },
|
||||||
|
{ key: 'editor_accepted', label: 'In Approval' },
|
||||||
|
// { key: 'approved', label: 'Ready for Review' },
|
||||||
|
// { key: 'reviewer_accepted', label: 'In Review' },
|
||||||
|
{ key: 'reviewed', label: 'Reviewed' },
|
||||||
|
{ key: 'published', label: 'Published' },
|
||||||
|
{ key: 'rejected_reviewer', label: 'Rejected by Reviewer' },
|
||||||
|
];
|
||||||
|
const getLabel = (key: string) => {
|
||||||
|
return datasetStates.find(s => s.key === key)?.label || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableActions = [
|
||||||
|
{ icon: mdiSquareEditOutline, label: 'Edit', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiTrayArrowDown, label: 'Receive', color: 'text-cyan-500' },
|
||||||
|
{ icon: mdiCheckDecagram, label: 'Approve (Send to Reviewer)', color: 'text-teal-600' },
|
||||||
|
{ icon: mdiAccountArrowLeft, label: 'Reject to Submitter', color: 'text-amber-600' },
|
||||||
|
{ icon: mdiAccountArrowRight, label: 'Reject to Reviewer', color: 'text-yellow-600' },
|
||||||
|
{ icon: mdiLibraryShelves, label: 'Classify', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiPublish, label: 'Publish', color: 'text-green-600' },
|
||||||
|
{ icon: mdiFingerprint, label: 'Mint DOI', color: 'text-cyan-600' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const truncateTitle = (text: string, length = 50) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > length ? text.substring(0, length) + '...' : text;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
|
||||||
<Head title="Editor Datasets" />
|
<Head title="Editor Datasets" />
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
|
|
||||||
|
|
||||||
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
{{ flash.message }}
|
{{ flash.message }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
@ -108,6 +124,80 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.error }}
|
{{ flash.error }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<CardBox class="mb-4">
|
||||||
|
<!-- Legend Header with Toggle -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between cursor-pointer select-none p-4 border-b border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors"
|
||||||
|
@click="toggleLegend"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
Legend - States & Actions
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ showLegend ? 'Click to hide' : 'Click to show' }}
|
||||||
|
</span>
|
||||||
|
<BaseIcon
|
||||||
|
:path="showLegend ? mdiChevronUp : mdiChevronDown"
|
||||||
|
:size="20"
|
||||||
|
class="text-gray-500 dark:text-gray-400 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible Legend Content -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
enter-from-class="max-h-0 opacity-0"
|
||||||
|
enter-to-class="max-h-96 opacity-100"
|
||||||
|
leave-active-class="transition-all duration-300 ease-in"
|
||||||
|
leave-from-class="max-h-96 opacity-100"
|
||||||
|
leave-to-class="max-h-0 opacity-0"
|
||||||
|
>
|
||||||
|
<div v-show="showLegend" class="overflow-hidden">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4">
|
||||||
|
<!-- State Colors Legend -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||||
|
</svg>
|
||||||
|
Dataset States
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div v-for="state in datasetStates" :key="state.key" class="flex items-center gap-2">
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full" :class="getStateColor(state.key)">
|
||||||
|
{{ state.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions Legend -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
Available Actions
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 gap-1.5 text-xs">
|
||||||
|
<div v-for="action in availableActions" :key="action.label" class="flex items-center gap-2">
|
||||||
|
<BaseIcon :path="action.icon" :size="16" :class="action.color" />
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">{{ action.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
<!-- table -->
|
<!-- table -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-6" has-table>
|
||||||
|
|
@ -115,172 +205,144 @@ const formatServerState = (state: string) => {
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Title</th>
|
||||||
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
|
<th>Submitter</th>
|
||||||
Title
|
<th>State</th>
|
||||||
</th>
|
<th>Editor</th>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Modified</th>
|
||||||
Submitter
|
<th v-if="can.edit || can.delete">Actions</th>
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
State
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
Editor
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
Date of last modification
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
|
|
||||||
<span class="sr-only">Actions</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody>
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
:class="[getRowClass(dataset)]">
|
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
|
||||||
<td data-label="Login"
|
<td data-label="Title">
|
||||||
class="py-4 whitespace-nowrap text-gray-700 table-title">
|
<div class="max-w-xs">
|
||||||
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
|
<span
|
||||||
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
|
class="text-gray-700 dark:text-gray-300 text-sm font-medium"
|
||||||
{{ user.login }}
|
:title="dataset.main_title"
|
||||||
</Link> -->
|
>
|
||||||
<!-- {{ user.id }} -->
|
{{ truncateTitle(dataset.main_title) }}
|
||||||
{{ dataset.main_title }}
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Submitter">
|
||||||
<div class="text-sm">{{ dataset.user.login }}</div>
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ dataset.user.login }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="State">
|
||||||
<div class="text-sm"> {{ formatServerState(dataset.server_state) }}</div>
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
||||||
class="inline-block relative ml-2 group">
|
:class="getStateColor(dataset.server_state)">
|
||||||
<button
|
{{ getLabel(dataset.server_state) }}
|
||||||
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
|
</span>
|
||||||
i
|
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
|
||||||
</button>
|
class="relative group">
|
||||||
<div
|
<button
|
||||||
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
|
class="w-5 h-5 rounded-full bg-gray-200 dark:bg-slate-700 text-gray-600 dark:text-gray-300 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300 dark:hover:bg-slate-600 transition-colors">
|
||||||
<p class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
i
|
||||||
{{ dataset.reject_reviewer_note }}
|
</button>
|
||||||
</p>
|
<div
|
||||||
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
|
class="absolute left-0 top-full mt-1 w-64 bg-white dark:bg-slate-800 shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100 border border-gray-200 dark:border-slate-700">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
||||||
|
{{ dataset.reject_reviewer_note }}
|
||||||
|
</p>
|
||||||
|
<div class="absolute -top-1 left-1 w-2 h-2 bg-white dark:bg-slate-800 border-l border-t border-gray-200 dark:border-slate-700 transform rotate-45">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700"
|
<td data-label="Editor" v-if="dataset.server_state === 'released'">
|
||||||
v-if="dataset.server_state === 'released'">
|
<span class="text-sm text-gray-600 dark:text-gray-400 italic" :title="dataset.server_date_modified">
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
Preferred: {{ dataset.preferred_reviewer }}
|
||||||
Preferred reviewer: {{ dataset.preferred_reviewer }}
|
</span>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700"
|
<td data-label="Editor"
|
||||||
v-else-if="dataset.server_state === 'editor_accepted' || dataset.server_state === 'rejected_reviewer'">
|
v-else-if="dataset.server_state === 'editor_accepted' || dataset.server_state === 'rejected_reviewer'">
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
<span class="text-sm text-gray-600 dark:text-gray-400 italic" :title="dataset.server_date_modified">
|
||||||
In approval by: {{ dataset.editor?.login }}
|
In approval: {{ dataset.editor?.login }}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700" v-else>
|
<td data-label="Editor" v-else>
|
||||||
<div class="text-sm">{{ dataset.editor?.login }}</div>
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ dataset.editor?.login || '—' }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Modified">
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.server_date_modified">
|
||||||
{{ dataset.server_date_modified }}
|
{{ dataset.server_date_modified }}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td v-if="can.edit || can.delete" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
|
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||||
<div type="justify-start lg:justify-end" class="grid grid-cols-2 gap-x-2 gap-y-2"
|
|
||||||
no-wrap>
|
|
||||||
|
|
||||||
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
|
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
|
||||||
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
|
||||||
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'" small
|
color="info" :icon="mdiTrayArrowDown" small
|
||||||
class="col-span-1" />
|
title="Receive edit task" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
|
||||||
color="info" :icon="mdiShareVariant" :label="'Approve'" small
|
color="success" :icon="mdiCheckDecagram" small
|
||||||
class="col-span-1" />
|
title="Approve (Send to Reviewer)" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
|
||||||
color="info" :icon="mdiUndo" label="Reject" small class="col-span-1">
|
color="danger" :icon="mdiAccountArrowLeft" small
|
||||||
</BaseButton>
|
title="Reject to Submitter" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.edit', [Number(dataset.id)])"
|
:route-name="stardust.route('editor.dataset.edit', [Number(dataset.id)])"
|
||||||
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small
|
color="info" :icon="mdiSquareEditOutline" small
|
||||||
class="col-span-1">
|
title="Edit" />
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.categorize', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.categorize', [dataset.id])"
|
||||||
color="info" :icon="mdiLibraryShelves" :label="'Classify'" small
|
color="info" :icon="mdiLibraryShelves" small
|
||||||
class="col-span-1">
|
title="Classify" />
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
||||||
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
|
||||||
color="info" :icon="mdiUndo" :label="'Reject To Reviewer'" small
|
color="warning" :icon="mdiAccountArrowRight" small
|
||||||
class="col-span-1" />
|
title="Reject to Reviewer" />
|
||||||
|
|
||||||
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
||||||
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
|
||||||
color="info" :icon="mdiBookEdit" :label="'Publish'" small
|
color="success" :icon="mdiPublish" small
|
||||||
class="col-span-1" />
|
title="Publish" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
|
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
|
||||||
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
|
||||||
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small
|
color="info" :icon="mdiFingerprint" small
|
||||||
class="col-span-1 last-in-row" />
|
title="Mint DOI" />
|
||||||
|
</BaseButtons>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Show warning message if datasets are not defined or empty -->
|
<div class="text-center py-12">
|
||||||
<div class="bg-yellow-200 text-yellow-800 rounded-md p-4">
|
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||||
<p>No datasets defined.</p>
|
<p class="text-lg font-medium mb-2">No datasets found</p>
|
||||||
<!-- You can add more descriptive text here -->
|
<p class="text-sm">Datasets will appear here when they are submitted</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- <BaseButton
|
|
||||||
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
|
||||||
:route-name="stardust.route('editor.dataset.edit', [dataset.id])" color="info"
|
|
||||||
:icon="mdiSquareEditOutline" :label="'Edit'" small /> -->
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<Pagination v-bind:data="datasets.meta" />
|
<Pagination v-bind:data="datasets.meta" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<style scoped lang="css">
|
|
||||||
.table-title {
|
|
||||||
max-width: 200px;
|
|
||||||
/* set a maximum width */
|
|
||||||
overflow: hidden;
|
|
||||||
/* hide overflow */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* show ellipsis for overflowed text */
|
|
||||||
white-space: nowrap;
|
|
||||||
/* prevent wrapping */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,40 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { ref, Ref } from 'vue';
|
import { ref, Ref } from 'vue';
|
||||||
import { mdiChartTimelineVariant, mdiGithub } from '@mdi/js';
|
import { mdiChartTimelineVariant, mdiGithub, mdiMapMarker, mdiCalendar, mdiLockOpenVariant } from '@mdi/js';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||||
import { MapOptions } from '@/Components/Map/MapOptions';
|
import { MapOptions } from '@/Components/Map/MapOptions';
|
||||||
// import { stardust } from '@eidellev/adonis-stardust/client';
|
|
||||||
import SearchMap from '@/Components/Map/SearchMap.vue';
|
import SearchMap from '@/Components/Map/SearchMap.vue';
|
||||||
import { OpensearchDocument } from '@/Dataset';
|
import { OpensearchDocument } from '@/Dataset';
|
||||||
|
|
||||||
// const fitBounds: LatLngBoundsExpression = [
|
|
||||||
// [46.4318173285, 9.47996951665],
|
|
||||||
// [49.0390742051, 16.9796667823],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// const mapId = 'map';
|
|
||||||
|
|
||||||
// const coverage = {
|
|
||||||
// x_min: undefined,
|
|
||||||
// y_min: undefined,
|
|
||||||
// x_max: undefined,
|
|
||||||
// y_max: undefined,
|
|
||||||
// elevation_min: undefined,
|
|
||||||
// elevation_max: undefined,
|
|
||||||
// elevation_absolut: undefined,
|
|
||||||
// depth_min: undefined,
|
|
||||||
// depth_max: undefined,
|
|
||||||
// depth_absolut: undefined,
|
|
||||||
// time_min: undefined,
|
|
||||||
// time_max: undefined,
|
|
||||||
// time_absolut: undefined,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Replace with your actual data
|
|
||||||
const datasets: Ref<OpensearchDocument[]> = ref([]);
|
const datasets: Ref<OpensearchDocument[]> = ref([]);
|
||||||
const openAccessLicences: Array<string> = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
|
const openAccessLicences: Array<string> = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
|
||||||
|
|
||||||
|
|
@ -48,58 +23,499 @@ const mapOptions: MapOptions = {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated :showAsideMenu="false">
|
<LayoutAuthenticated :showAsideMenu="false">
|
||||||
|
|
||||||
<Head title="Map" />
|
<Head title="Map" />
|
||||||
|
|
||||||
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
|
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Tethys Map" main>
|
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Tethys Map" main>
|
||||||
<BaseButton href="https://gitea.geosphere.at/geolba/tethys" target="_blank" :icon="mdiGithub"
|
<BaseButton
|
||||||
label="Star on GeoSPhere Forgejo" color="contrast" rounded-full small />
|
href="https://gitea.geosphere.at/geolba/tethys"
|
||||||
<!-- <BaseButton :route-name="stardust.route('app.login.show')" label="Login" color="white" rounded-full small /> -->
|
target="_blank"
|
||||||
|
:icon="mdiGithub"
|
||||||
|
label="Star on GeoSphere Forgejo"
|
||||||
|
color="contrast"
|
||||||
|
rounded-full
|
||||||
|
small
|
||||||
|
/>
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
<!-- <SectionBannerStarOnGitea /> -->
|
<!-- Map Component with enhanced styling -->
|
||||||
|
<div class="map-wrapper">
|
||||||
|
<SearchMap :datasets="datasets" :map-options="mapOptions"></SearchMap>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- <CardBox> -->
|
<!-- Results Header -->
|
||||||
<!-- <div id="map" class="map-container mapDesktop mt-6 mb-6 rounded-2xl py-12 px-6 text-center">
|
<div v-if="datasets.length > 0" class="results-header">
|
||||||
<DrawControlComponent ref="draw" :preserve="false" :mapId="mapId" :southWest="southWest"
|
<h2 class="results-title">
|
||||||
:northEast="northEast">
|
<span class="results-count">{{ datasets.length }}</span>
|
||||||
</DrawControlComponent>
|
{{ datasets.length === 1 ? 'Dataset' : 'Datasets' }} Found
|
||||||
</div> -->
|
</h2>
|
||||||
<SearchMap :datasets="datasets" :map-options="mapOptions"></SearchMap>
|
<p class="results-subtitle">Click on any card to view details</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div d="search-result-list-wrapper" class="flex flex-wrap col-span-24 h-full">
|
<!-- Enhanced Results Grid -->
|
||||||
<div v-for="dataset in datasets" :key="dataset.id" class="w-full sm:w-1/2 md:w-1/3 lg:w-1/4 p-4">
|
<div class="results-grid">
|
||||||
<div class="bg-white rounded shadow p-6">
|
<div
|
||||||
<h3 class="text-lg leading-tight text-gray-500 dark:text-slate-400">
|
v-for="(dataset, index) in datasets"
|
||||||
|
:key="dataset.id"
|
||||||
|
class="dataset-card"
|
||||||
|
:style="{ animationDelay: `${index * 50}ms` }"
|
||||||
|
>
|
||||||
|
<!-- Card Header with Icon -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path :d="mdiMapMarker" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="card-type">{{ dataset.doctype }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Content -->
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">
|
||||||
{{ dataset.title_output }}
|
{{ dataset.title_output }}
|
||||||
</h3>
|
</h3>
|
||||||
<!-- <h2 class="text-xl font-bold mb-2">{{ dataset.title_output }}</h2> -->
|
<p class="card-abstract">
|
||||||
<p class="text-gray-700 mb-2">{{ dataset.abstract_output }}</p>
|
{{ dataset.abstract_output }}
|
||||||
<div class="text-sm text-gray-600">
|
|
||||||
<div v-for="author in dataset.author" :key="author" class="mb-1">{{ author }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4">
|
|
||||||
<span
|
|
||||||
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
|
||||||
{{ dataset.year }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
|
||||||
{{ dataset.language }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<span class="label"><i class="fas fa-file"></i> {{ dataset.doctype }}</span>
|
|
||||||
<!-- <span>Licence: {{ document.licence }}</span> -->
|
|
||||||
<span v-if="openAccessLicences.includes(dataset.licence)" class="label titlecase"><i
|
|
||||||
class="fas fa-lock-open"></i> Open Access</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Authors Section -->
|
||||||
|
<div v-if="dataset.author && dataset.author.length > 0" class="card-authors">
|
||||||
|
<div class="author-label">Authors:</div>
|
||||||
|
<div class="author-list">
|
||||||
|
<span v-for="(author, idx) in dataset.author.slice(0, 3)" :key="idx" class="author-tag">
|
||||||
|
{{ author }}
|
||||||
|
</span>
|
||||||
|
<span v-if="dataset.author.length > 3" class="author-more"> +{{ dataset.author.length - 3 }} more </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Footer with Metadata -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="metadata-tags">
|
||||||
|
<span class="tag tag-year">
|
||||||
|
<svg class="tag-icon" viewBox="0 0 24 24">
|
||||||
|
<path :d="mdiCalendar" />
|
||||||
|
</svg>
|
||||||
|
{{ dataset.year }}
|
||||||
|
</span>
|
||||||
|
<span class="tag tag-language">
|
||||||
|
{{ dataset.language?.toUpperCase() }}
|
||||||
|
</span>
|
||||||
|
<span v-if="openAccessLicences.includes(dataset.licence)" class="tag tag-open-access">
|
||||||
|
<svg class="tag-icon" viewBox="0 0 24 24">
|
||||||
|
<path :d="mdiLockOpenVariant" />
|
||||||
|
</svg>
|
||||||
|
Open Access
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hover Effect Overlay -->
|
||||||
|
<div class="card-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="datasets.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="empty-title">No datasets selected</h3>
|
||||||
|
<p class="empty-description">Draw a rectangle on the map to search for datasets in that area</p>
|
||||||
|
</div>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<!-- </section> -->
|
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Map Wrapper */
|
||||||
|
.map-wrapper {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-wrapper:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Header */
|
||||||
|
.results-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .results-header {
|
||||||
|
border-bottom-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .results-title {
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-count {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #65dc21 0%, #357c06 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-subtitle {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .results-subtitle {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Grid */
|
||||||
|
.results-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dataset Card */
|
||||||
|
.dataset-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
animation: fadeInUp 0.5s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .dataset-card {
|
||||||
|
background: #1f2937;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .dataset-card:hover {
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Header */
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #65dc21 0%, #357c06 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-type {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Content */
|
||||||
|
.card-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-title {
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-abstract {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-abstract {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Authors Section */
|
||||||
|
.card-authors {
|
||||||
|
padding: 0 1.5rem 1rem;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-authors {
|
||||||
|
border-top-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .author-label {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #4b5563;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .author-tag {
|
||||||
|
color: #d1d5db;
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-more {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .author-more {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Footer */
|
||||||
|
.card-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-footer {
|
||||||
|
background: #111827;
|
||||||
|
border-top-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-icon {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-year {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tag-year {
|
||||||
|
background: #1e3a8a;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-language {
|
||||||
|
background: #fce7f3;
|
||||||
|
color: #9f1239;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tag-language {
|
||||||
|
background: #831843;
|
||||||
|
color: #fbcfe8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-open-access {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tag-open-access {
|
||||||
|
background: #064e3b;
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Overlay */
|
||||||
|
.card-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(101, 220, 33, 0.1) 0%, rgba(53, 124, 6, 0.1) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-card:hover .card-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-state {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-icon {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-title {
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-description {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.results-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiAlertBoxOutline, mdiGlasses, mdiReiterate } from '@mdi/js';
|
import { mdiAlertBoxOutline, mdiGlasses, mdiAccountArrowLeft, mdiChevronDown, mdiChevronUp } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
|
|
@ -25,63 +26,83 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
// user: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flash: ComputedRef<any> = computed(() => {
|
const flash: ComputedRef<any> = computed(() => {
|
||||||
// let test = usePage();
|
|
||||||
// console.log(test);
|
|
||||||
return usePage().props.flash;
|
return usePage().props.flash;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legend visibility state with localStorage persistence
|
||||||
|
const showLegend = ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedState = localStorage.getItem('reviewerDatasetLegendVisible');
|
||||||
|
if (savedState !== null) {
|
||||||
|
showLegend.value = savedState === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLegend = () => {
|
||||||
|
showLegend.value = !showLegend.value;
|
||||||
|
localStorage.setItem('reviewerDatasetLegendVisible', String(showLegend.value));
|
||||||
|
};
|
||||||
|
|
||||||
const getRowClass = (dataset) => {
|
const getRowClass = (dataset) => {
|
||||||
// (props.options ? 'select' : props.type)
|
// Return Tailwind classes that will be defined in tailwind.config
|
||||||
let rowclass = '';
|
const stateClasses = {
|
||||||
if (dataset.server_state == 'approved') {
|
'approved': 'bg-approved dark:bg-approved-dark',
|
||||||
rowclass = 'bg-approved';
|
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
} else if (dataset.server_state == 'rejected_reviewer') {
|
'rejected_to_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
rowclass = 'bg-rejected-reviewer';
|
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
|
||||||
} else if (dataset.server_state == 'reviewed') {
|
};
|
||||||
rowclass = 'bg-reviewed';
|
|
||||||
} else if (dataset.server_state == 'released') {
|
return stateClasses[dataset.server_state] || '';
|
||||||
rowclass = 'bg-released';
|
|
||||||
} else if (dataset.server_state == 'published') {
|
|
||||||
rowclass = 'bg-published';
|
|
||||||
} else if (dataset.server_state == 'rejected_to_reviewer') {
|
|
||||||
rowclass = 'bg-rejected-reviewer';
|
|
||||||
} else {
|
|
||||||
rowclass = '';
|
|
||||||
}
|
|
||||||
return rowclass;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// New method to format server state
|
// Method to get state badge color
|
||||||
const formatServerState = (state: string) => {
|
const getStateColor = (state: string) => {
|
||||||
if (state === 'inprogress') {
|
const stateColors = {
|
||||||
return 'draft';
|
'inprogress': 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200',
|
||||||
} else if (state === 'released') {
|
'released': 'bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-200',
|
||||||
return 'submitted';
|
'editor_accepted': 'bg-teal-200 text-teal-900 dark:bg-teal-900 dark:text-teal-200',
|
||||||
} else if (state === 'approved') {
|
'rejected_reviewer': 'bg-amber-200 text-amber-900 dark:bg-amber-900 dark:text-amber-200',
|
||||||
return 'ready for review';
|
'rejected_to_reviewer': 'bg-yellow-200 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
} else if (state === 'reviewer_accepted') {
|
'rejected_editor': 'bg-rose-300 text-rose-900 dark:bg-rose-900 dark:text-rose-200',
|
||||||
return 'in review';
|
'reviewed': 'bg-yellow-300 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
}
|
'published': 'bg-green-300 text-green-900 dark:bg-green-900 dark:text-green-200',
|
||||||
return state; // Return the original state for other cases
|
'approved': 'bg-cyan-200 text-cyan-900 dark:bg-cyan-900 dark:text-cyan-200',
|
||||||
|
'reviewer_accepted': 'bg-lime-200 text-lime-900 dark:bg-lime-900 dark:text-lime-200',
|
||||||
|
};
|
||||||
|
return stateColors[state] || 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const truncateTitle = (text: string, length = 50) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > length ? text.substring(0, length) + '...' : text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamic legend definitions
|
||||||
|
const datasetStates = [
|
||||||
|
{ key: 'approved', label: 'Ready for Review' },
|
||||||
|
{ key: 'rejected_to_reviewer', label: 'Rejected To Reviewer' }
|
||||||
|
];
|
||||||
|
const getLabel = (key: string) => {
|
||||||
|
return datasetStates.find(s => s.key === key)?.label || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableActions = [
|
||||||
|
{ icon: mdiGlasses, label: 'View / Review', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiAccountArrowLeft, label: 'Reject to Editor', color: 'text-yellow-600' },
|
||||||
|
];
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
<Head title="Reviewer Datasets" />
|
||||||
<Head title="Dataset List" />
|
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
|
|
||||||
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
{{ flash.message }}
|
{{ flash.message }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
@ -92,86 +113,162 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.error }}
|
{{ flash.error }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<CardBox class="mb-4">
|
||||||
|
<!-- Legend Header with Toggle -->
|
||||||
|
<!-- <div
|
||||||
|
class="flex items-center justify-between cursor-pointer select-none p-4 border-b border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors"
|
||||||
|
@click="toggleLegend"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
Legend - States & Actions
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ showLegend ? 'Click to hide' : 'Click to show' }}
|
||||||
|
</span>
|
||||||
|
<BaseIcon
|
||||||
|
:path="showLegend ? mdiChevronUp : mdiChevronDown"
|
||||||
|
:size="20"
|
||||||
|
class="text-gray-500 dark:text-gray-400 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Collapsible Legend Content -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
enter-from-class="max-h-0 opacity-0"
|
||||||
|
enter-to-class="max-h-96 opacity-100"
|
||||||
|
leave-active-class="transition-all duration-300 ease-in"
|
||||||
|
leave-from-class="max-h-96 opacity-100"
|
||||||
|
leave-to-class="max-h-0 opacity-0"
|
||||||
|
>
|
||||||
|
<div v-show="showLegend" class="overflow-hidden">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4">
|
||||||
|
<!-- State Colors Legend -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||||
|
</svg>
|
||||||
|
Dataset States
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div v-for="state in datasetStates" :key="state.key" class="flex items-center gap-2">
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full" :class="getStateColor(state.key)">
|
||||||
|
{{ state.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions Legend -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
Available Actions
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 gap-1.5 text-xs">
|
||||||
|
<div v-for="action in availableActions" :key="action.label" class="flex items-center gap-2">
|
||||||
|
<BaseIcon :path="action.icon" :size="16" :class="action.color" />
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">{{ action.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
<!-- table -->
|
<!-- table -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-6" has-table>
|
||||||
<div v-if="props.datasets.data.length > 0">
|
<div v-if="props.datasets.data.length > 0">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Title</th>
|
||||||
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
|
<th>ID</th>
|
||||||
Title
|
<th>State</th>
|
||||||
</th>
|
<th>Editor</th>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Remaining Time</th>
|
||||||
ID
|
<th v-if="can.edit || can.delete">Actions</th>
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
|
|
||||||
State
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
|
|
||||||
Editor
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
Remaining Time
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
|
|
||||||
<span class="sr-only">Actions</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="[getRowClass(dataset)]">
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
<td data-label="Login"
|
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
|
||||||
class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Title">
|
||||||
<div class="text-sm table-title">{{ dataset.main_title }}</div>
|
<div class="max-w-xs">
|
||||||
|
<span
|
||||||
|
class="text-gray-700 dark:text-gray-300 text-sm font-medium"
|
||||||
|
:title="dataset.main_title"
|
||||||
|
>
|
||||||
|
{{ truncateTitle(dataset.main_title) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="ID">
|
||||||
<div class="text-sm">{{ dataset.id }}</div>
|
<span class="text-sm text-gray-700 dark:text-gray-300 font-mono">
|
||||||
|
{{ dataset.id }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="State">
|
||||||
<div class="text-sm">{{ formatServerState(dataset.server_state) }}</div>
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="dataset.server_state === 'rejected_to_reviewer' && dataset.reject_editor_note"
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
||||||
class="inline-block relative ml-2 group">
|
:class="getStateColor(dataset.server_state)">
|
||||||
<button
|
{{ getLabel(dataset.server_state) }}
|
||||||
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
|
</span>
|
||||||
i
|
<div v-if="dataset.server_state === 'rejected_to_reviewer' && dataset.reject_editor_note"
|
||||||
</button>
|
class="relative group">
|
||||||
<div
|
<button
|
||||||
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
|
class="w-5 h-5 rounded-full bg-gray-200 dark:bg-slate-700 text-gray-600 dark:text-gray-300 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300 dark:hover:bg-slate-600 transition-colors">
|
||||||
<p class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
i
|
||||||
{{ dataset.reject_editor_note }}
|
</button>
|
||||||
</p>
|
<div
|
||||||
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
|
class="absolute left-0 top-full mt-1 w-64 bg-white dark:bg-slate-800 shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100 border border-gray-200 dark:border-slate-700">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
||||||
|
{{ dataset.reject_editor_note }}
|
||||||
|
</p>
|
||||||
|
<div class="absolute -top-1 left-1 w-2 h-2 bg-white dark:bg-slate-800 border-l border-t border-gray-200 dark:border-slate-700 transform rotate-45">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td data-label="Editor">
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
<div class="text-sm">{{ dataset.editor?.login }}</div>
|
{{ dataset.editor?.login || '—' }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Remaining Time">
|
||||||
<div class="text-sm" :title="dataset.remaining_time">
|
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.remaining_time">
|
||||||
{{ dataset.remaining_time + ' days' }}
|
{{ dataset.remaining_time }} days
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td v-if="can.reject" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
|
|
||||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
||||||
:route-name="stardust.route('reviewer.dataset.review', [dataset.id])"
|
:route-name="stardust.route('reviewer.dataset.review', [dataset.id])"
|
||||||
color="info" :icon="mdiGlasses" :label="'View'" small />
|
color="info" :icon="mdiGlasses" small
|
||||||
|
title="View / Review" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
||||||
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])"
|
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])"
|
||||||
color="info" :icon="mdiReiterate" :label="'Reject'" small />
|
color="warning" :icon="mdiAccountArrowLeft" small
|
||||||
|
title="Reject to Editor" />
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -179,21 +276,16 @@ const formatServerState = (state: string) => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Show warning message if datasets are not defined or empty -->
|
<div class="text-center py-12">
|
||||||
<div class="bg-yellow-200 text-yellow-800 rounded-md p-4">
|
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||||
<p>No datasets defined.</p>
|
<p class="text-lg font-medium mb-2">No datasets found</p>
|
||||||
<!-- You can add more descriptive text here -->
|
<p class="text-sm">Datasets will appear here when they are ready for review</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- <BaseButton
|
|
||||||
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
|
||||||
:route-name="stardust.route('editor.dataset.edit', [dataset.id])" color="info"
|
|
||||||
:icon="mdiSquareEditOutline" :label="'Edit'" small /> -->
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<Pagination v-bind:data="datasets.meta" />
|
<Pagination v-bind:data="datasets.meta" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|
@ -201,15 +293,24 @@ const formatServerState = (state: string) => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
.table-title {
|
/* Background colors are now defined in tailwind.config.js */
|
||||||
max-width: 200px;
|
/* .bg-approved {
|
||||||
/* set a maximum width */
|
@apply bg-approved dark:bg-approved-dark;
|
||||||
overflow: hidden;
|
|
||||||
/* hide overflow */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* show ellipsis for overflowed text */
|
|
||||||
white-space: nowrap;
|
|
||||||
/* prevent wrapping */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-rejected-reviewer {
|
||||||
|
@apply bg-rejected-reviewer dark:bg-rejected-reviewer-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-reviewed {
|
||||||
|
@apply bg-reviewed dark:bg-reviewed-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-released {
|
||||||
|
@apply bg-released dark:bg-released-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-published {
|
||||||
|
@apply bg-published dark:bg-published-dark;
|
||||||
|
} */
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen, mdiLibraryShelves } from '@mdi/js';
|
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen, mdiLibraryShelves, mdiChevronDown, mdiChevronUp } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
import Pagination from '@/Components/Pagination.vue';
|
import Pagination from '@/Components/Pagination.vue';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
|
import Label from '@/Components/unused/Label.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
datasets: {
|
datasets: {
|
||||||
|
|
@ -26,63 +27,120 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
// user: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flash: ComputedRef<any> = computed(() => {
|
const flash: ComputedRef<any> = computed(() => {
|
||||||
// let test = usePage();
|
|
||||||
// console.log(test);
|
|
||||||
return usePage().props.flash;
|
return usePage().props.flash;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legend visibility state with localStorage persistence
|
||||||
|
const showLegend = ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedState = localStorage.getItem('submitterDatasetLegendVisible');
|
||||||
|
if (savedState !== null) {
|
||||||
|
showLegend.value = savedState === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLegend = () => {
|
||||||
|
showLegend.value = !showLegend.value;
|
||||||
|
localStorage.setItem('submitterDatasetLegendVisible', String(showLegend.value));
|
||||||
|
};
|
||||||
|
|
||||||
const validStates = ['inprogress', 'rejected_editor'];
|
const validStates = ['inprogress', 'rejected_editor'];
|
||||||
|
|
||||||
|
|
||||||
const getRowClass = (dataset) => {
|
const getRowClass = (dataset) => {
|
||||||
// (props.options ? 'select' : props.type)
|
// Return Tailwind classes that will be defined in tailwind.config
|
||||||
let rowclass = '';
|
const stateClasses = {
|
||||||
if (dataset.server_state == 'inprogress') {
|
'inprogress': 'bg-inprogress dark:bg-inprogress-dark',
|
||||||
rowclass = 'bg-inprogress';
|
'released': 'bg-released dark:bg-released-dark',
|
||||||
} else if (dataset.server_state == 'released') {
|
'editor_accepted': 'bg-editor-accepted dark:bg-editor-accepted-dark',
|
||||||
rowclass = 'bg-released';
|
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
} else if (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer') {
|
'rejected_to_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
rowclass = 'bg-editor-accepted';
|
|
||||||
} else if (dataset.server_state == 'approved') {
|
'approved': 'bg-approved dark:bg-approved-dark',
|
||||||
rowclass = 'bg-approved';
|
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
|
||||||
} else if (dataset.server_state == 'reviewed') {
|
'published': 'bg-published dark:bg-published-dark',
|
||||||
rowclass = 'bg-reviewed';
|
'rejected_editor': 'bg-rejected-editor dark:bg-rejected-editor-dark',
|
||||||
} else if (dataset.server_state == 'rejected_editor') {
|
};
|
||||||
rowclass = 'bg-rejected-editor';
|
|
||||||
} else {
|
return stateClasses[dataset.server_state] || '';
|
||||||
rowclass = '';
|
|
||||||
}
|
|
||||||
return rowclass;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// New method to format server state
|
// New method to format server state
|
||||||
const formatServerState = (state: string) => {
|
// const formatServerState = (state: string) => {
|
||||||
if (state === 'inprogress') {
|
// if (state === 'inprogress') {
|
||||||
return 'draft';
|
// return 'draft';
|
||||||
} else if (state === 'released') {
|
// } else if (state === 'released') {
|
||||||
return 'submitted';
|
// return 'submitted';
|
||||||
} else if (state === 'approved') {
|
// } else if (state === 'editor_accepted') {
|
||||||
return 'ready for review';
|
// return 'in approval';
|
||||||
} else if (state === 'reviewer_accepted') {
|
// } else if (state === 'approved') {
|
||||||
return 'in review';
|
// return 'ready for review';
|
||||||
}
|
// } else if (state === 'reviewer_accepted') {
|
||||||
return state; // Return the original state for other cases
|
// return 'in review';
|
||||||
|
// } else if (state === 'rejected_editor') {
|
||||||
|
// return 'rejected by editor';
|
||||||
|
// }
|
||||||
|
// return state;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Method to get state badge color
|
||||||
|
// Method to get state badge color
|
||||||
|
|
||||||
|
const getStateColor = (state: string) => {
|
||||||
|
const stateColors = {
|
||||||
|
'inprogress': 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200',
|
||||||
|
'released': 'bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-200',
|
||||||
|
'editor_accepted': 'bg-teal-200 text-teal-900 dark:bg-teal-900 dark:text-teal-200',
|
||||||
|
'rejected_reviewer': 'bg-amber-200 text-amber-900 dark:bg-amber-900 dark:text-amber-200',
|
||||||
|
'rejected_to_reviewer': 'bg-yellow-200 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
'rejected_editor': 'bg-rose-300 text-rose-900 dark:bg-rose-900 dark:text-rose-200',
|
||||||
|
'reviewed': 'bg-yellow-300 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
'published': 'bg-green-300 text-green-900 dark:bg-green-900 dark:text-green-200',
|
||||||
|
'approved': 'bg-cyan-200 text-cyan-900 dark:bg-cyan-900 dark:text-cyan-200',
|
||||||
|
'reviewer_accepted': 'bg-lime-200 text-lime-900 dark:bg-lime-900 dark:text-lime-200',
|
||||||
|
};
|
||||||
|
return stateColors[state] || 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dynamic legend definitions
|
||||||
|
const datasetStates = [
|
||||||
|
{ key: 'inprogress', label: 'Draft' },
|
||||||
|
{ key: 'released', label: 'Submitted' },
|
||||||
|
{ key: 'editor_accepted', label: 'In Approval' },
|
||||||
|
{ key: 'approved', label: 'Ready for Review' },
|
||||||
|
{ key: 'reviewer_accepted', label: 'In Review' },
|
||||||
|
{ key: 'reviewed', label: 'Reviewed' },
|
||||||
|
// { key: 'published', label: 'Published' },
|
||||||
|
{ key: 'rejected_editor', label: 'Rejected by Editor' },
|
||||||
|
{ key: 'rejected_reviewer', label: 'Rejected by Reviewer' },
|
||||||
|
{ key: 'rejected_to_reviewer', label: 'Rejected To Reviewer' }
|
||||||
|
];
|
||||||
|
const getLabel = (key: string) => {
|
||||||
|
return datasetStates.find(s => s.key === key)?.label || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableActions = [
|
||||||
|
{ icon: mdiLockOpen, label: 'Release (Submit)', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiSquareEditOutline, label: 'Edit', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiLibraryShelves, label: 'Classify', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiTrashCan, label: 'Delete', color: 'text-red-500' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const truncateTitle = (text: string, length = 50) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > length ? text.substring(0, length) + '...' : text;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
<Head title="My Datasets" />
|
||||||
<Head title="Dataset List" />
|
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
|
|
||||||
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
{{ flash.message }}
|
{{ flash.message }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
@ -90,86 +148,170 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.warning }}
|
{{ flash.warning }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
||||||
<!-- table -->
|
<!-- Legend -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-4">
|
||||||
<table class="w-full table-fixed">
|
<!-- Legend Header with Toggle -->
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
class="flex items-center justify-between cursor-pointer select-none p-4 border-b border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors"
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
@click="toggleLegend"
|
||||||
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
|
>
|
||||||
Dataset Title
|
<div class="flex items-center gap-2">
|
||||||
</th>
|
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
|
</svg>
|
||||||
Server State
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
</th>
|
Legend - States & Actions
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
</h3>
|
||||||
Date of last modification
|
</div>
|
||||||
</th>
|
<div class="flex items-center gap-2">
|
||||||
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
<span class="sr-only">Actions</span>
|
{{ showLegend ? 'Click to hide' : 'Click to show' }}
|
||||||
</th>
|
</span>
|
||||||
</tr>
|
<BaseIcon
|
||||||
</thead>
|
:path="showLegend ? mdiChevronUp : mdiChevronDown"
|
||||||
|
:size="20"
|
||||||
|
class="text-gray-500 dark:text-gray-400 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<!-- Collapsible Legend Content -->
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="getRowClass(dataset)">
|
<transition
|
||||||
<td data-label="Login"
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
class="py-4 whitespace-nowrap text-gray-700 table-title">
|
enter-from-class="max-h-0 opacity-0"
|
||||||
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
|
enter-to-class="max-h-96 opacity-100"
|
||||||
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
|
leave-active-class="transition-all duration-300 ease-in"
|
||||||
{{ user.login }}
|
leave-from-class="max-h-96 opacity-100"
|
||||||
</Link> -->
|
leave-to-class="max-h-0 opacity-0"
|
||||||
<!-- {{ user.id }} -->
|
>
|
||||||
{{ dataset.main_title }}
|
<div v-show="showLegend" class="overflow-hidden">
|
||||||
</td>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4">
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<!-- State Colors Legend -->
|
||||||
{{ formatServerState(dataset.server_state) }}
|
<div>
|
||||||
<div v-if="dataset.server_state === 'rejected_editor' && dataset.reject_editor_note"
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
class="inline-block relative ml-2 group">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<button
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||||
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
|
</svg>
|
||||||
i
|
Dataset States
|
||||||
</button>
|
</h3>
|
||||||
<div
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
|
<div v-for="state in datasetStates" :key="state.key" class="flex items-center gap-2">
|
||||||
<p
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full" :class="getStateColor(state.key)">
|
||||||
class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
{{ state.label }}
|
||||||
{{ dataset.reject_editor_note }}
|
</span>
|
||||||
</p>
|
|
||||||
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
|
|
||||||
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
|
<!-- Actions Legend -->
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
<div>
|
||||||
{{ dataset.server_date_modified }}
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
Available Actions
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 gap-1.5 text-xs">
|
||||||
|
<div v-for="action in availableActions" :key="action.label" class="flex items-center gap-2">
|
||||||
|
<BaseIcon :path="action.icon" :size="16" :class="action.color" />
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">{{ action.label }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td
|
</div>
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
|
</div>
|
||||||
<BaseButtons v-if="validStates.includes(dataset.server_state)"
|
</transition>
|
||||||
type="justify-start lg:justify-end" no-wrap>
|
</CardBox>
|
||||||
<!-- release created dataset -->
|
|
||||||
<BaseButton v-if="can.edit"
|
<!-- table -->
|
||||||
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
|
<CardBox class="mb-6" has-table>
|
||||||
:icon="mdiLockOpen" :label="'Release'" small />
|
<div v-if="props.datasets.data.length > 0">
|
||||||
<BaseButton v-if="can.edit"
|
<table>
|
||||||
:route-name="stardust.route('dataset.edit', [dataset.id])" color="info"
|
<thead>
|
||||||
:icon="mdiSquareEditOutline" :label="'Edit'" small />
|
<tr>
|
||||||
<BaseButton v-if="can.edit"
|
<th>Title</th>
|
||||||
:route-name="stardust.route('dataset.categorize', [dataset.id])" color="info"
|
<th>State</th>
|
||||||
:icon="mdiLibraryShelves" :label="'Classify'" small />
|
<th>Modified</th>
|
||||||
<BaseButton v-if="can.delete" color="danger"
|
<th v-if="can.edit || can.delete">Actions</th>
|
||||||
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
</tr>
|
||||||
small />
|
</thead>
|
||||||
</BaseButtons>
|
|
||||||
</td>
|
<tbody>
|
||||||
</tr>
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
</tbody>
|
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
|
||||||
</table>
|
<td data-label="Title">
|
||||||
|
<div class="max-w-xs">
|
||||||
|
<span
|
||||||
|
class="text-gray-700 dark:text-gray-300 text-sm font-medium"
|
||||||
|
:title="dataset.main_title"
|
||||||
|
>
|
||||||
|
{{ truncateTitle(dataset.main_title) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td data-label="State">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
||||||
|
:class="getStateColor(dataset.server_state)">
|
||||||
|
{{ getLabel(dataset.server_state) }}
|
||||||
|
</span>
|
||||||
|
<div v-if="dataset.server_state === 'rejected_editor' && dataset.reject_editor_note"
|
||||||
|
class="relative group">
|
||||||
|
<button
|
||||||
|
class="w-5 h-5 rounded-full bg-gray-200 dark:bg-slate-700 text-gray-600 dark:text-gray-300 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300 dark:hover:bg-slate-600 transition-colors">
|
||||||
|
i
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-full mt-1 w-64 bg-white dark:bg-slate-800 shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100 border border-gray-200 dark:border-slate-700">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
||||||
|
{{ dataset.reject_editor_note }}
|
||||||
|
</p>
|
||||||
|
<div class="absolute -top-1 left-1 w-2 h-2 bg-white dark:bg-slate-800 border-l border-t border-gray-200 dark:border-slate-700 transform rotate-45">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td data-label="Modified">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.server_date_modified">
|
||||||
|
{{ dataset.server_date_modified }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="can.edit || can.delete" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
|
<BaseButtons v-if="validStates.includes(dataset.server_state)"
|
||||||
|
type="justify-start lg:justify-end" no-wrap>
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
|
||||||
|
:icon="mdiLockOpen" small
|
||||||
|
title="Release (Submit)" />
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.edit', [dataset.id])" color="info"
|
||||||
|
:icon="mdiSquareEditOutline" small
|
||||||
|
title="Edit" />
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.categorize', [dataset.id])" color="info"
|
||||||
|
:icon="mdiLibraryShelves" small
|
||||||
|
title="Classify" />
|
||||||
|
<BaseButton v-if="can.delete" color="danger"
|
||||||
|
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
||||||
|
small
|
||||||
|
title="Delete" />
|
||||||
|
</BaseButtons>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||||
|
<p class="text-lg font-medium mb-2">No datasets found</p>
|
||||||
|
<p class="text-sm">Create your first dataset to get started</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<Pagination v-bind:data="datasets.meta" />
|
<Pagination v-bind:data="datasets.meta" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,56 +321,28 @@ const formatServerState = (state: string) => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
.table-title {
|
/* Background colors are now defined in tailwind.config.js */
|
||||||
max-width: 200px;
|
/* .bg-inprogress {
|
||||||
/* set a maximum width */
|
@apply bg-inprogress dark:bg-inprogress-dark;
|
||||||
overflow: hidden;
|
|
||||||
/* hide overflow */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* show ellipsis for overflowed text */
|
|
||||||
white-space: nowrap;
|
|
||||||
/* prevent wrapping */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-fixed {
|
.bg-released {
|
||||||
table-layout: fixed;
|
@apply bg-released dark:bg-released-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .pure-table tr.released {
|
.bg-editor-accepted {
|
||||||
background-color: rgb(52 211 153);
|
@apply bg-editor-accepted dark:bg-editor-accepted-dark;
|
||||||
color: gray;
|
}
|
||||||
|
|
||||||
|
.bg-approved {
|
||||||
|
@apply bg-approved dark:bg-approved-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-reviewed {
|
||||||
|
@apply bg-reviewed dark:bg-reviewed-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-rejected-editor {
|
||||||
|
@apply bg-rejected-editor dark:bg-rejected-editor-dark;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
/* .pure-table tr.inprogress {
|
|
||||||
padding: 0.8em;
|
|
||||||
background-color: rgb(94 234 212);
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.editor_accepted {
|
|
||||||
background-color: rgb(125 211 252);
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.rejected_reviewer {
|
|
||||||
padding: 0.8em;
|
|
||||||
background-color: orange;
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.rejected_editor {
|
|
||||||
background-color: orange;
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.reviewed {
|
|
||||||
background-color: yellow;
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.approved {
|
|
||||||
background-color: rgb(86, 86, 241);
|
|
||||||
color: whitesmoke;
|
|
||||||
|
|
||||||
}*/
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -13,8 +13,49 @@ module.exports = {
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
'radio-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")",
|
'radio-checked':
|
||||||
'checkbox-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E\")",
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")",
|
||||||
|
'checkbox-checked':
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E\")",
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
// Draft / In Progress - Light blue-gray
|
||||||
|
'draft': 'rgb(224 242 254)', // sky-100
|
||||||
|
'draft-dark': 'rgb(12 74 110 / 0.3)', // sky-900/30
|
||||||
|
'inprogress': 'rgb(224 242 254)', // sky-100
|
||||||
|
'inprogress-dark': 'rgb(12 74 110 / 0.3)', // sky-900/30
|
||||||
|
|
||||||
|
// Released / Submitted - Bright blue
|
||||||
|
'released': 'rgb(191 219 254)', // blue-200
|
||||||
|
'released-dark': 'rgb(30 58 138 / 0.3)', // blue-900/30
|
||||||
|
|
||||||
|
// Editor Accepted - Blue-green (teal)
|
||||||
|
'editor-accepted': 'rgb(204 251 241)', // teal-100
|
||||||
|
'editor-accepted-dark': 'rgb(19 78 74 / 0.3)', // teal-900/30
|
||||||
|
|
||||||
|
// Rejected by Reviewer - Yellow-orange (amber)
|
||||||
|
'rejected-reviewer': 'rgb(254 243 199)', // amber-100
|
||||||
|
'rejected-reviewer-dark': 'rgb(120 53 15 / 0.3)', // amber-900/30
|
||||||
|
|
||||||
|
// Rejected by Editor - Rose/Red (back to submitter)
|
||||||
|
'rejected-editor': 'rgb(254 205 211)', // rose-200
|
||||||
|
'rejected-editor-dark': 'rgb(136 19 55 / 0.3)', // rose-900/30
|
||||||
|
|
||||||
|
// Approved / Ready for Review - Cyan (blue-green)
|
||||||
|
'approved': 'rgb(207 250 254)', // cyan-100
|
||||||
|
'approved-dark': 'rgb(22 78 99 / 0.3)', // cyan-900/30
|
||||||
|
|
||||||
|
// Reviewer Accepted / In Review - Lime yellow-green
|
||||||
|
'reviewer-accepted': 'rgb(236 252 203)', // lime-100
|
||||||
|
'reviewer-accepted-dark': 'rgb(54 83 20 / 0.3)', // lime-900/30
|
||||||
|
|
||||||
|
// Reviewed - Soft yellow
|
||||||
|
'reviewed': 'rgb(254 240 138)', // yellow-200
|
||||||
|
'reviewed-dark': 'rgb(113 63 18 / 0.3)', // yellow-900/30
|
||||||
|
|
||||||
|
// Published - Fresh green
|
||||||
|
'published': 'rgb(187 247 208)', // green-200
|
||||||
|
'published-dark': 'rgb(20 83 45 / 0.3)', // green-900/30
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
'primary': '#22C55E',
|
'primary': '#22C55E',
|
||||||
|
|
@ -30,7 +71,7 @@ module.exports = {
|
||||||
'lime': {
|
'lime': {
|
||||||
DEFAULT: '#BFCE40',
|
DEFAULT: '#BFCE40',
|
||||||
dark: 'rgba(5,46,55,0.7)',
|
dark: 'rgba(5,46,55,0.7)',
|
||||||
50: '#FBFCF7',
|
50: '#FBFCF7',
|
||||||
100: '#F8FBE1',
|
100: '#F8FBE1',
|
||||||
200: '#EEF69E',
|
200: '#EEF69E',
|
||||||
300: '#DCEC53',
|
300: '#DCEC53',
|
||||||
|
|
@ -40,7 +81,7 @@ module.exports = {
|
||||||
700: '#357C06',
|
700: '#357C06',
|
||||||
800: '#295B09',
|
800: '#295B09',
|
||||||
900: '#20450A',
|
900: '#20450A',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||||
|
|
@ -106,7 +147,7 @@ module.exports = {
|
||||||
{ values: theme('asideScrollbars') },
|
{ values: theme('asideScrollbars') },
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
plugin(function({ addUtilities }) {
|
plugin(function ({ addUtilities }) {
|
||||||
const newUtilities = {
|
const newUtilities = {
|
||||||
'.drag-none': {
|
'.drag-none': {
|
||||||
'-webkit-user-drag': 'none',
|
'-webkit-user-drag': 'none',
|
||||||
|
|
@ -115,8 +156,8 @@ module.exports = {
|
||||||
'-o-user-drag': 'none',
|
'-o-user-drag': 'none',
|
||||||
'user-drag': 'none',
|
'user-drag': 'none',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
addUtilities(newUtilities)
|
addUtilities(newUtilities);
|
||||||
}),
|
}),
|
||||||
// As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default
|
// As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default
|
||||||
// require('@tailwindcss/line-clamp'),
|
// require('@tailwindcss/line-clamp'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue