Merge pull request #519 from wanderer-industries/jumpgates

Jumpgates
This commit is contained in:
Dmitry Popov
2025-09-23 20:23:25 +04:00
committed by GitHub
20 changed files with 377 additions and 194 deletions

View File

@@ -8,6 +8,10 @@
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
}
.ConnectionBridge {
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
}
.ConnectionSave {
background-image: linear-gradient(207deg, transparent, var(--conn-save));
}

View File

@@ -1,10 +1,3 @@
import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import classes from './ContextMenuConnection.module.scss';
import {
MASS_STATE_NAMES,
MASS_STATE_NAMES_ORDER,
@@ -13,7 +6,16 @@ import {
SHIP_SIZES_NAMES_SHORT,
SHIP_SIZES_SIZE,
} from '@/hooks/Mapper/components/map/constants.ts';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { ContextMenu } from 'primereact/contextmenu';
import { MenuItem } from 'primereact/menuitem';
import React, { RefObject, useMemo } from 'react';
import { Edge } from 'reactflow';
import classes from './ContextMenuConnection.module.scss';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
export interface ContextMenuConnectionProps {
contextMenuRef: RefObject<ContextMenu>;
@@ -21,6 +23,7 @@ export interface ContextMenuConnectionProps {
onChangeTimeState(): void;
onChangeMassState(state: MassState): void;
onChangeShipSizeStatus(state: ShipSizeStatus): void;
onChangeType(type: ConnectionType): void;
onToggleMassSave(isLocked: boolean): void;
onHide(): void;
edge?: Edge<SolarSystemConnection>;
@@ -32,6 +35,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
onChangeTimeState,
onChangeMassState,
onChangeShipSizeStatus,
onChangeType,
onToggleMassSave,
onHide,
edge,
@@ -41,84 +45,127 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
return [];
}
const sourceInfo = getSystemStaticInfo(edge.data?.source);
const targetInfo = getSystemStaticInfo(edge.data?.target);
const bothNullsec =
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
const isWormhole = edge.data?.type !== ConnectionType.gate;
if (edge.data?.type === ConnectionType.bridge) {
return [
{
label: `Set as Wormhole`,
icon: 'pi hero-arrow-uturn-left',
command: () => onChangeType(ConnectionType.wormhole),
},
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
if (edge.data?.type === ConnectionType.gate) {
return [
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
return [
...(isWormhole
{
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeMassState(x),
})),
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeShipSizeStatus(x),
command: () => onChangeMassState(x),
})),
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
}),
command: () => onChangeShipSizeStatus(x),
})),
},
...(bothNullsec
? [
{
label: `Set as Bridge`,
icon: 'pi hero-forward',
command: () => onChangeType(ConnectionType.bridge),
},
]
: []),
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
}, [
edge,
onChangeTimeState,
onDeleteConnection,
onChangeType,
onChangeShipSizeStatus,
onToggleMassSave,
onChangeMassState,
]);
return (
<>

View File

@@ -56,7 +56,8 @@ export const KillsCounter = ({
className={className}
tooltipClassName="!px-0"
size={size}
interactive={true}
interactive
smallPaddings
>
{children}
</WdTooltipWrapper>

View File

@@ -46,7 +46,13 @@ export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIc
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
>
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
<WdTooltipWrapper
content={pilotTooltipContent}
position={TooltipPosition.right}
offset={0}
interactive={true}
smallPaddings
>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(classes.localCounter, {

View File

@@ -29,6 +29,13 @@
&.Gate {
stroke: #9aff40;
}
&.Bridge {
stroke: #9aff40;
stroke-dasharray: 10 5;
stroke-linecap: round;
}
}
.EdgePathFront {

View File

@@ -9,6 +9,7 @@ import { PrimeIcons } from 'primereact/api';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
const MAP_TRANSLATES: Record<string, string> = {
[Position.Top]: 'translate(-48%, 0%)',
@@ -42,7 +43,9 @@ export const SHIP_SIZES_COLORS = {
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
const isWormhole = data?.type !== ConnectionType.gate;
const isWormhole = data?.type === ConnectionType.wormhole;
const isGate = data?.type === ConnectionType.gate;
const isBridge = data?.type === ConnectionType.bridge;
const {
data: { isThickConnections },
@@ -55,9 +58,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getBezierPath;
const [edgePath, labelX, labelY] = method({
const [edgePath, labelX, labelY] = getBezierPath({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
sourcePosition: sourcePos,
@@ -67,7 +68,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
}, [isThickConnections, sourceNode, targetNode]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -81,7 +82,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.Tick]: isThickConnections,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.Hovered]: hovered,
[classes.Gate]: !isWormhole,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
})}
d={path}
markerEnd={markerEnd}
@@ -95,7 +97,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: !isWormhole,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
})}
d={path}
markerEnd={markerEnd}
@@ -147,6 +150,19 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
</WdTooltipWrapper>
)}
{isBridge && (
<WdTooltipWrapper
content="Ansiblex Jump Bridge"
position={TooltipPosition.top}
className={clsx(
classes.LinkLabel,
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
)}
>
B
</WdTooltipWrapper>
)}
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
<WdTooltipWrapper
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}

View File

@@ -58,6 +58,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
</InfoDrawer>
</div>
}
smallPaddings
>
<div className={clsx(classes.Box, whClassStyle)}>
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">

View File

@@ -716,11 +716,12 @@ export const STATUS_CLASSES: Record<number, string> = {
[STATUSES.dangerous]: 'eve-system-status-dangerous',
};
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
export const TYPE_NAMES = {
[ConnectionType.wormhole]: 'Wormhole',
[ConnectionType.gate]: 'Gate',
[ConnectionType.bridge]: 'Jumpgate',
};
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];

View File

@@ -15,3 +15,12 @@ export const isKnownSpace = (wormholeClassID: number) => {
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
return spaces.includes(wormholeClassID);
};
export const isNullsecSpace = (wormholeClassID: number) => {
switch (wormholeClassID) {
case SOLAR_SYSTEM_CLASS_IDS.ns:
return true;
}
return false;
};

View File

@@ -49,87 +49,91 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
useCommandsCharacters();
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated:
case Commands.characterActivityData:
case Commands.trackingCharactersData:
case Commands.updateActivity:
case Commands.updateTracking:
case Commands.userSettingsUpdated:
// do nothing
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated:
case Commands.characterActivityData:
case Commands.trackingCharactersData:
case Commands.updateActivity:
case Commands.updateTracking:
case Commands.userSettingsUpdated:
// do nothing
break;
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
}, []);
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
},
[],
);
};

View File

@@ -118,6 +118,7 @@ $homeDark30: color.adjust($homeBase, $lightness: -30%);
--conn-time-eol: #7452c3e3;
--conn-frigate: #325d88;
--conn-bridge: rgba(135, 185, 93, 0.85);
--conn-save: rgba(155, 102, 45, 0.85);
--selected-item-bg: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,22 +1,21 @@
import classes from './Connections.module.scss';
import { Sidebar } from 'primereact/sidebar';
import { useEffect, useMemo, useState, useCallback } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import {
ConnectionType,
ConnectionOutput,
ConnectionInfoOutput,
ConnectionOutput,
ConnectionType,
OutCommand,
Passage,
SolarSystemConnection,
} from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { Sidebar } from 'primereact/sidebar';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { useCallback, useEffect, useMemo, useState } from 'react';
import classes from './Connections.module.scss';
import { PassageCard } from './PassageCard';
import { InfoDrawer, SystemView } from '@/hooks/Mapper/components/ui-kit';
import { InfoDrawer, SystemView, TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { PassageCard } from './PassageCard';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
@@ -78,7 +77,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
}, [connections, selectedConnection]);
const isWormhole = useMemo(() => {
return cnInfo?.type !== ConnectionType.gate;
return cnInfo?.type === ConnectionType.wormhole;
}, [cnInfo]);
const [passages, setPassages] = useState<Passage[]>([]);

View File

@@ -18,6 +18,7 @@ export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
content: (() => React.ReactNode) | React.ReactNode;
targetSelector?: string;
interactive?: boolean;
smallPaddings?: boolean;
}
export interface OffsetPosition {
@@ -47,6 +48,7 @@ export const WdTooltip = forwardRef(
position: tPosition = TooltipPosition.default,
offset = 5,
interactive = false,
smallPaddings = false,
className,
...restProps
}: TooltipProps,
@@ -264,10 +266,14 @@ export const WdTooltip = forwardRef(
ref={tooltipRef}
className={clsx(
classes.tooltip,
interactive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute px-1 py-1',
'absolute px-2 py-1',
'border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
pos == null && 'invisible',
{
'pointer-events-auto': interactive,
'pointer-events-none': !interactive,
invisible: pos == null,
'!px-1': smallPaddings,
},
className,
)}
style={{

View File

@@ -8,13 +8,26 @@ export type WdTooltipWrapperProps = {
content?: (() => ReactNode) | ReactNode;
size?: TooltipSize;
interactive?: boolean;
smallPaddings?: boolean;
tooltipClassName?: string;
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
Omit<TooltipProps, 'content'>;
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
(
{ className, children, content, offset, position, targetSelector, interactive, size, tooltipClassName, ...props },
{
className,
children,
content,
offset,
position,
targetSelector,
interactive,
smallPaddings,
size,
tooltipClassName,
...props
},
forwardedRef,
) => {
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
@@ -31,6 +44,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
position={position}
content={content}
interactive={interactive}
smallPaddings={smallPaddings}
targetSelector={finalTargetSelector}
className={clsx(size && sizeClass(size), tooltipClassName)}
/>

View File

@@ -1,6 +1,7 @@
export enum ConnectionType {
wormhole,
gate,
bridge,
}
export enum MassState {

View File

@@ -168,6 +168,7 @@ defmodule WandererApp.Api.MapConnection do
# where 0 - Wormhole
# where 1 - Gate
# where 2 - Bridge
attribute :type, :integer do
default(0)

View File

@@ -72,6 +72,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
@connection_time_status_eol 1
@connection_type_wormhole 0
@connection_type_stargate 1
@connection_type_bridge 2
@medium_ship_size 1
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
@@ -146,6 +147,18 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state
end
def update_connection_type(
%{map_id: map_id} = state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
character_id: character_id
} = _connection_info,
type
) do
state
end
def get_connection_info(
%{map_id: map_id} = _state,
%{
@@ -239,7 +252,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
} ->
connection_start_time = get_start_time(map_id, connection_id)
type != @connection_type_stargate &&
type == @connection_type_wormhole &&
DateTime.diff(DateTime.utc_now(), connection_start_time, :hour) >=
connection_auto_eol_hours &&
is_connection_valid(
@@ -295,7 +308,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
)
not is_connection_exist ||
(type != @connection_type_stargate && is_connection_valid &&
(type == @connection_type_wormhole && is_connection_valid &&
DateTime.diff(DateTime.utc_now(), connection_mark_eol_time, :hour) >=
connection_auto_expire_hours - connection_auto_eol_hours +
+connection_eol_expire_timeout_hours)

View File

@@ -1,4 +1,4 @@
<section class="prose prose-lg max-w-full w-full leading-normal tracking-normal text-indigo-400 bg-cover bg-fixed flex items-center justify-center">
<section class="prose prose-lg max-w-full w-full leading-normal tracking-normal text-indigo-400 bg-cover bg-fixed flex items-center justify-center">
<canvas id="bg-canvas"></canvas>
<div class="h-full w-full flex flex-col items-center">
<!--Main-->
@@ -26,23 +26,71 @@
</div>
</div>
</div>
<div class="carousel carousel-center bg-neutral rounded-box max-w-[80%] space-x-4 p-4">
<%= for post <- @posts do %>
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
<div class="artboard-horizontal phone-1 relative hover:text-white mt-10">
<img
class="rounded-lg shadow-lg block !w-[400px] !h-[200px] opacity-75"
src={post.cover_image_uri}
/>
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
<div id="posts-container" class="bg-neutral rounded-box max-w-[90%] p-4 max-h-[60vh] overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<%= for post <- @posts do %>
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
<div class="artboard-horizontal phone-1 relative hover:text-white">
<img
class="rounded-lg shadow-lg block !w-[300px] !h-[180px] opacity-75"
src={post.cover_image_uri}
/>
<div class="absolute rounded-lg top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
</div>
<h3 class="absolute bottom-4 left-14 !text-md font-bold break-normal pt-6 pb-2 ccp-font text-white">
{post.title}
</h3>
</div>
<h3 class="absolute bottom-4 left-14 font-bold break-normal pt-6 pb-2 ccp-font text-white">
{post.title}
</h3>
</div>
</.link>
<% end %>
</.link>
<% end %>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const postsContainer = document.getElementById('posts-container');
if (!postsContainer) return;
let scrollSpeed = 0.5; // pixels per frame
let isScrolling = true;
let scrollDirection = 1; // 1 for down, -1 for up
function autoScroll() {
if (!isScrolling) return;
const maxScroll = postsContainer.scrollHeight - postsContainer.clientHeight;
if (maxScroll <= 0) return; // No need to scroll if content fits
postsContainer.scrollTop += scrollSpeed * scrollDirection;
// Reverse direction when reaching top or bottom
if (postsContainer.scrollTop >= maxScroll) {
scrollDirection = -1;
} else if (postsContainer.scrollTop <= 0) {
scrollDirection = 1;
}
requestAnimationFrame(autoScroll);
}
// Pause scrolling on hover
postsContainer.addEventListener('mouseenter', () => {
isScrolling = false;
});
// Resume scrolling when mouse leaves
postsContainer.addEventListener('mouseleave', () => {
isScrolling = true;
requestAnimationFrame(autoScroll);
});
// Start autoscroll after a delay
setTimeout(() => {
requestAnimationFrame(autoScroll);
}, 2000);
});
</script>
<%!-- <div class="carousel carousel-center !bg-neutral rounded-box max-w-4xl space-x-6 p-4">
</div> --%>

View File

@@ -34,7 +34,11 @@
</button>
</:action>
</.table>
<.link :if={@allow_acl_creation} class="btn mt-2 w-full btn-neutral rounded-none" patch={~p"/access-lists/new"}>
<.link
:if={@allow_acl_creation}
class="btn mt-2 w-full btn-neutral rounded-none"
patch={~p"/access-lists/new"}
>
<.icon name="hero-plus-solid" class="w-6 h-6" />
<h3 class="card-title text-center text-md">New Access List</h3>
</.link>
@@ -142,10 +146,10 @@
placeholder="Select an owner"
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
/>
<!-- Divider between above inputs and the API key section -->
<hr class="my-4 border-gray-600" />
<!-- API Key Section with grid layout -->
<div class="mt-2">
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>

View File

@@ -1,7 +1,7 @@
%{
title: "Guide: Systems and Connections API",
author: "Wanderer Team",
cover_image_uri: "/images/news/05-07-systems/api-endpoints.png",
cover_image_uri: "/images/news/03-05-api/swagger-ui.png",
tags: ~w(api map systems connections documentation),
description: "Detailed guide for Wanderer's systems and connections API endpoints, including batch operations, updates, and deletions."
}
@@ -912,4 +912,4 @@ If you have questions about these endpoints or need assistance, please reach out
Fly safe,
**The Wanderer Team**
----
----