Merge pull request 'merge-upstream-5.0.1' (#66) from merge-upstream-5.0.1 into main
Reviewed-on: #66
This commit is contained in:
commit
20d0b49941
|
@ -4,7 +4,6 @@ stages:
|
||||||
- install
|
- install
|
||||||
- check
|
- check
|
||||||
- build-js
|
- build-js
|
||||||
- sentry
|
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
- upload
|
- upload
|
||||||
|
@ -93,21 +92,6 @@ build-frontend:
|
||||||
needs:
|
needs:
|
||||||
- lint-front
|
- lint-front
|
||||||
|
|
||||||
sentry-commit:
|
|
||||||
stage: sentry
|
|
||||||
image: getsentry/sentry-cli
|
|
||||||
script:
|
|
||||||
- echo "Create a new release $CI_COMMIT_TAG"
|
|
||||||
- sentry-cli releases new $CI_COMMIT_TAG
|
|
||||||
- sentry-cli releases set-commits $CI_COMMIT_TAG --auto
|
|
||||||
- sentry-cli releases files $CI_COMMIT_TAG upload-sourcemaps priv/static/assets/
|
|
||||||
- sentry-cli releases finalize $CI_COMMIT_TAG
|
|
||||||
- echo "Finalized release for $CI_COMMIT_TAG"
|
|
||||||
needs:
|
|
||||||
- build-frontend
|
|
||||||
only:
|
|
||||||
- tags@framasoft/mobilizon
|
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
stage: check
|
stage: check
|
||||||
before_script:
|
before_script:
|
||||||
|
@ -162,6 +146,8 @@ vitest:
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
stage: test
|
stage: test
|
||||||
|
except:
|
||||||
|
- tags@framasoft/mobilizon
|
||||||
services:
|
services:
|
||||||
- name: postgis/postgis:16-3.4
|
- name: postgis/postgis:16-3.4
|
||||||
alias: postgres
|
alias: postgres
|
||||||
|
|
|
@ -17,7 +17,7 @@ mixRelease rec {
|
||||||
|
|
||||||
# This has to be kept in sync with the version in mix.exs and package.json!
|
# This has to be kept in sync with the version in mix.exs and package.json!
|
||||||
# Otherwise the nginx routing isn't going to work properly.
|
# Otherwise the nginx routing isn't going to work properly.
|
||||||
version = "5.0.0-beta.1";
|
version = "5.1.0";
|
||||||
|
|
||||||
inherit src;
|
inherit src;
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.mobilizon-postgresql.serviceConfig.Restart = "on-failure";
|
||||||
|
|
||||||
services.postgresql.package = pkgs.postgresql_14;
|
services.postgresql.package = pkgs.postgresql_14;
|
||||||
|
|
||||||
security.pki.certificateFiles = [ certs.ca.cert ];
|
security.pki.certificateFiles = [ certs.ca.cert ];
|
||||||
|
|
|
@ -57,7 +57,8 @@ defmodule Mobilizon.GraphQL.API.Search do
|
||||||
current_actor_id: Map.get(args, :current_actor_id),
|
current_actor_id: Map.get(args, :current_actor_id),
|
||||||
exclude_my_groups: Map.get(args, :exclude_my_groups, false),
|
exclude_my_groups: Map.get(args, :exclude_my_groups, false),
|
||||||
exclude_stale_actors: true,
|
exclude_stale_actors: true,
|
||||||
local_only: Map.get(args, :search_target, :internal) == :self
|
local_only: Map.get(args, :search_target, :internal) == :self,
|
||||||
|
sort_by: Map.get(args, :sort_by)
|
||||||
],
|
],
|
||||||
page,
|
page,
|
||||||
limit
|
limit
|
||||||
|
|
|
@ -76,7 +76,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
limit: limit,
|
limit: limit,
|
||||||
order_by: order_by,
|
order_by: order_by,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
longevents: longevents,
|
long_events: long_events,
|
||||||
location: location,
|
location: location,
|
||||||
radius: radius
|
radius: radius
|
||||||
},
|
},
|
||||||
|
@ -84,7 +84,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
)
|
)
|
||||||
when limit < @event_max_limit do
|
when limit < @event_max_limit do
|
||||||
{:ok,
|
{:ok,
|
||||||
Events.list_events(page, limit, order_by, direction, true, longevents, location, radius)}
|
Events.list_events(page, limit, order_by, direction, true, long_events, location, radius)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_events(
|
def list_events(
|
||||||
|
|
|
@ -32,6 +32,11 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
field(:description, :string, description: "The event's description")
|
field(:description, :string, description: "The event's description")
|
||||||
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
||||||
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
||||||
|
|
||||||
|
field(:long_event, :boolean,
|
||||||
|
description: "Whether the event is a long event (activity) or not"
|
||||||
|
)
|
||||||
|
|
||||||
field(:status, :event_status, description: "Status of the event")
|
field(:status, :event_status, description: "Status of the event")
|
||||||
field(:visibility, :event_visibility, description: "The event's visibility")
|
field(:visibility, :event_visibility, description: "The event's visibility")
|
||||||
field(:join_options, :event_join_options, description: "The event's visibility")
|
field(:join_options, :event_join_options, description: "The event's visibility")
|
||||||
|
@ -395,7 +400,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
description: "Direction for the sort"
|
description: "Direction for the sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
arg(:longevents, :boolean,
|
arg(:long_events, :boolean,
|
||||||
default_value: nil,
|
default_value: nil,
|
||||||
description: "if mention filter in or out long events"
|
description: "if mention filter in or out long events"
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,6 +17,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||||
field(:title, :string, description: "The event's title")
|
field(:title, :string, description: "The event's title")
|
||||||
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
||||||
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
||||||
|
|
||||||
|
field(:long_event, :boolean,
|
||||||
|
description: "Whether the event is a long event (activity) or not"
|
||||||
|
)
|
||||||
|
|
||||||
field(:status, :event_status, description: "Status of the event")
|
field(:status, :event_status, description: "Status of the event")
|
||||||
field(:picture, :media, description: "The event's picture")
|
field(:picture, :media, description: "The event's picture")
|
||||||
field(:physical_address, :address, description: "The event's physical address")
|
field(:physical_address, :address, description: "The event's physical address")
|
||||||
|
@ -52,6 +57,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||||
field(:title, :string, description: "The event's title")
|
field(:title, :string, description: "The event's title")
|
||||||
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
||||||
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
||||||
|
|
||||||
|
field(:long_event, :boolean,
|
||||||
|
description: "Whether the event is a long event (activity) or not"
|
||||||
|
)
|
||||||
|
|
||||||
field(:status, :event_status, description: "Status of the event")
|
field(:status, :event_status, description: "Status of the event")
|
||||||
field(:picture, :media, description: "The event's picture")
|
field(:picture, :media, description: "The event's picture")
|
||||||
field(:physical_address, :address, description: "The event's physical address")
|
field(:physical_address, :address, description: "The event's physical address")
|
||||||
|
@ -171,7 +181,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||||
|
|
||||||
enum :search_group_sort_options do
|
enum :search_group_sort_options do
|
||||||
value(:match_desc, description: "The pertinence of the result")
|
value(:match_desc, description: "The pertinence of the result")
|
||||||
value(:member_count_desc, description: "The members count of the group")
|
value(:member_count_asc, description: "The members count of the group ascendant order")
|
||||||
|
value(:member_count_desc, description: "The members count of the group descendent order")
|
||||||
|
value(:created_at_asc, description: "When the group was created ascendant order")
|
||||||
|
value(:created_at_desc, description: "When the group was created descendent order")
|
||||||
|
value(:last_event_activity, description: "Last event activity of the group")
|
||||||
end
|
end
|
||||||
|
|
||||||
enum :search_event_sort_options do
|
enum :search_event_sort_options do
|
||||||
|
@ -273,7 +287,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||||
description: "Radius around the location to search in"
|
description: "Radius around the location to search in"
|
||||||
)
|
)
|
||||||
|
|
||||||
arg(:longevents, :boolean, description: "if mention filter in or out long events")
|
arg(:long_events, :boolean, description: "if mention filter in or out long events")
|
||||||
|
|
||||||
arg(:bbox, :string, description: "The bbox to search events into")
|
arg(:bbox, :string, description: "The bbox to search events into")
|
||||||
arg(:zoom, :integer, description: "The zoom level for searching events")
|
arg(:zoom, :integer, description: "The zoom level for searching events")
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule Mobilizon.Actors do
|
||||||
alias Mobilizon.Actors.{Actor, Bot, Follower, Member}
|
alias Mobilizon.Actors.{Actor, Bot, Follower, Member}
|
||||||
alias Mobilizon.Addresses.Address
|
alias Mobilizon.Addresses.Address
|
||||||
alias Mobilizon.Crypto
|
alias Mobilizon.Crypto
|
||||||
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Events.FeedToken
|
alias Mobilizon.Events.FeedToken
|
||||||
alias Mobilizon.Medias
|
alias Mobilizon.Medias
|
||||||
alias Mobilizon.Service.Workers
|
alias Mobilizon.Service.Workers
|
||||||
|
@ -518,7 +519,6 @@ defmodule Mobilizon.Actors do
|
||||||
query = from(a in Actor)
|
query = from(a in Actor)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> distinct([q], q.id)
|
|
||||||
|> actor_by_username_or_name_query(term)
|
|> actor_by_username_or_name_query(term)
|
||||||
|> maybe_join_address(
|
|> maybe_join_address(
|
||||||
Keyword.get(options, :location),
|
Keyword.get(options, :location),
|
||||||
|
@ -532,8 +532,56 @@ defmodule Mobilizon.Actors do
|
||||||
|> filter_by_minimum_visibility(Keyword.get(options, :minimum_visibility, :public))
|
|> filter_by_minimum_visibility(Keyword.get(options, :minimum_visibility, :public))
|
||||||
|> filter_suspended(false)
|
|> filter_suspended(false)
|
||||||
|> filter_out_anonymous_actor_id(anonymous_actor_id)
|
|> filter_out_anonymous_actor_id(anonymous_actor_id)
|
||||||
|
# order_by
|
||||||
|
|> actor_order(Keyword.get(options, :sort_by, :match_desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# sort by most recent id if "best match"
|
||||||
|
defp actor_order(query, :match_desc) do
|
||||||
|
query
|
||||||
|
|> order_by([q], desc: q.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_order(query, :last_event_activity) do
|
||||||
|
query
|
||||||
|
|> join(:left, [q], e in Event, on: e.attributed_to_id == q.id)
|
||||||
|
|> group_by([q, e], q.id)
|
||||||
|
|> order_by([q, e], [
|
||||||
|
# put groups with no events at the end of the list
|
||||||
|
fragment("MAX(?) IS NULL", e.updated_at),
|
||||||
|
# last edited event of the group
|
||||||
|
desc: max(e.updated_at),
|
||||||
|
# sort group with no event by id
|
||||||
|
desc: q.id
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_order(query, :member_count_asc) do
|
||||||
|
query
|
||||||
|
|> join(:left, [q], m in Member, on: m.parent_id == q.id)
|
||||||
|
|> group_by([q, m], q.id)
|
||||||
|
|> order_by([q, m], asc: count(m.id), asc: q.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_order(query, :member_count_desc) do
|
||||||
|
query
|
||||||
|
|> join(:left, [q], m in Member, on: m.parent_id == q.id)
|
||||||
|
|> group_by([q, m], q.id)
|
||||||
|
|> order_by([q, m], desc: count(m.id), desc: q.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_order(query, :created_at_asc) do
|
||||||
|
query
|
||||||
|
|> order_by([q], asc: q.inserted_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_order(query, :created_at_desc) do
|
||||||
|
query
|
||||||
|
|> order_by([q], desc: q.inserted_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_order(query, _), do: query
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a group by its title.
|
Gets a group by its title.
|
||||||
"""
|
"""
|
||||||
|
@ -1394,16 +1442,6 @@ defmodule Mobilizon.Actors do
|
||||||
^username
|
^username
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> order_by(
|
|
||||||
[a],
|
|
||||||
fragment(
|
|
||||||
"word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc",
|
|
||||||
a.preferred_username,
|
|
||||||
^username,
|
|
||||||
a.name,
|
|
||||||
^username
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec maybe_join_address(
|
@spec maybe_join_address(
|
||||||
|
|
|
@ -66,6 +66,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
participants: [Actor.t()],
|
participants: [Actor.t()],
|
||||||
contacts: [Actor.t()],
|
contacts: [Actor.t()],
|
||||||
language: String.t(),
|
language: String.t(),
|
||||||
|
long_event: boolean,
|
||||||
metadata: [EventMetadata.t()]
|
metadata: [EventMetadata.t()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +90,8 @@ defmodule Mobilizon.Events.Event do
|
||||||
:picture_id,
|
:picture_id,
|
||||||
:physical_address_id,
|
:physical_address_id,
|
||||||
:attributed_to_id,
|
:attributed_to_id,
|
||||||
:language
|
:language,
|
||||||
|
:long_event
|
||||||
]
|
]
|
||||||
@attrs @required_attrs ++ @optional_attrs
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
@ -102,6 +104,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
field(:slug, :string)
|
field(:slug, :string)
|
||||||
field(:description, :string)
|
field(:description, :string)
|
||||||
field(:ends_on, :utc_datetime)
|
field(:ends_on, :utc_datetime)
|
||||||
|
field(:long_event, :boolean, virtual: true, default: nil)
|
||||||
field(:title, :string)
|
field(:title, :string)
|
||||||
field(:status, EventStatus, default: :confirmed)
|
field(:status, EventStatus, default: :confirmed)
|
||||||
field(:draft, :boolean, default: false)
|
field(:draft, :boolean, default: false)
|
||||||
|
|
|
@ -12,6 +12,8 @@ defmodule Mobilizon.Events do
|
||||||
import Mobilizon.Storage.Ecto
|
import Mobilizon.Storage.Ecto
|
||||||
import Mobilizon.Events.Utils, only: [calculate_notification_time: 1]
|
import Mobilizon.Events.Utils, only: [calculate_notification_time: 1]
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
alias Ecto.{Changeset, Multi}
|
alias Ecto.{Changeset, Multi}
|
||||||
|
|
||||||
alias Mobilizon.Actors.{Actor, Follower}
|
alias Mobilizon.Actors.{Actor, Follower}
|
||||||
|
@ -141,6 +143,7 @@ defmodule Mobilizon.Events do
|
||||||
url
|
url
|
||||||
|> event_by_url_query()
|
|> event_by_url_query()
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -153,6 +156,7 @@ defmodule Mobilizon.Events do
|
||||||
|> event_by_url_query()
|
|> event_by_url_query()
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Repo.one!()
|
|> Repo.one!()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -167,6 +171,7 @@ defmodule Mobilizon.Events do
|
||||||
|> filter_draft()
|
|> filter_draft()
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Repo.one!()
|
|> Repo.one!()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -180,6 +185,7 @@ defmodule Mobilizon.Events do
|
||||||
|> filter_draft()
|
|> filter_draft()
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec check_if_event_has_instance_follow(String.t(), integer()) :: boolean()
|
@spec check_if_event_has_instance_follow(String.t(), integer()) :: boolean()
|
||||||
|
@ -199,6 +205,7 @@ defmodule Mobilizon.Events do
|
||||||
|> event_by_uuid_query()
|
|> event_by_uuid_query()
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -212,6 +219,7 @@ defmodule Mobilizon.Events do
|
||||||
|> filter_not_event_uuid(not_event_uuid)
|
|> filter_not_event_uuid(not_event_uuid)
|
||||||
|> filter_draft()
|
|> filter_draft()
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -366,7 +374,7 @@ defmodule Mobilizon.Events do
|
||||||
atom,
|
atom,
|
||||||
boolean,
|
boolean,
|
||||||
boolean | nil,
|
boolean | nil,
|
||||||
string | nil,
|
String.t() | nil,
|
||||||
float | nil
|
float | nil
|
||||||
) :: Page.t(Event.t())
|
) :: Page.t(Event.t())
|
||||||
def list_events(
|
def list_events(
|
||||||
|
@ -375,7 +383,7 @@ defmodule Mobilizon.Events do
|
||||||
sort \\ :begins_on,
|
sort \\ :begins_on,
|
||||||
direction \\ :asc,
|
direction \\ :asc,
|
||||||
is_future \\ true,
|
is_future \\ true,
|
||||||
longevents \\ nil,
|
long_events \\ nil,
|
||||||
location \\ nil,
|
location \\ nil,
|
||||||
radius \\ nil
|
radius \\ nil
|
||||||
) do
|
) do
|
||||||
|
@ -386,12 +394,13 @@ defmodule Mobilizon.Events do
|
||||||
|> maybe_join_address(%{location: location, radius: radius})
|
|> maybe_join_address(%{location: location, radius: radius})
|
||||||
|> events_for_location(%{location: location, radius: radius})
|
|> events_for_location(%{location: location, radius: radius})
|
||||||
|> filter_future_events(is_future)
|
|> filter_future_events(is_future)
|
||||||
|> events_for_longevents(longevents)
|
|> events_for_long_events(long_events)
|
||||||
|> filter_public_visibility()
|
|> filter_public_visibility()
|
||||||
|> filter_draft()
|
|> filter_draft()
|
||||||
|> filter_cancelled_events()
|
|> filter_cancelled_events()
|
||||||
|> filter_local_or_from_followed_instances_events()
|
|> filter_local_or_from_followed_instances_events()
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec stream_events_for_sitemap :: Enum.t()
|
@spec stream_events_for_sitemap :: Enum.t()
|
||||||
|
@ -413,6 +422,7 @@ defmodule Mobilizon.Events do
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> event_order_by(sort, direction)
|
|> event_order_by(sort, direction)
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -425,6 +435,7 @@ defmodule Mobilizon.Events do
|
||||||
|> events_by_tags_query(limit)
|
|> events_by_tags_query(limit)
|
||||||
|> filter_draft()
|
|> filter_draft()
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -440,6 +451,7 @@ defmodule Mobilizon.Events do
|
||||||
actor_id
|
actor_id
|
||||||
|> do_list_public_events_for_actor()
|
|> do_list_public_events_for_actor()
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -467,6 +479,7 @@ defmodule Mobilizon.Events do
|
||||||
|> do_list_public_events_for_actor()
|
|> do_list_public_events_for_actor()
|
||||||
|> event_filter_begins_on(DateTime.utc_now(), nil)
|
|> event_filter_begins_on(DateTime.utc_now(), nil)
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec do_list_public_events_for_actor(integer()) :: Ecto.Query.t()
|
@spec do_list_public_events_for_actor(integer()) :: Ecto.Query.t()
|
||||||
|
@ -485,6 +498,7 @@ defmodule Mobilizon.Events do
|
||||||
|> event_for_actor_query(desc: :begins_on)
|
|> event_for_actor_query(desc: :begins_on)
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_simple_organized_events_for_group(Actor.t(), integer | nil, integer | nil) ::
|
@spec list_simple_organized_events_for_group(Actor.t(), integer | nil, integer | nil) ::
|
||||||
|
@ -516,10 +530,13 @@ defmodule Mobilizon.Events do
|
||||||
group_id
|
group_id
|
||||||
|> event_for_group_query()
|
|> event_for_group_query()
|
||||||
|> event_filter_visibility(visibility)
|
|> event_filter_visibility(visibility)
|
||||||
|> event_filter_begins_on(after_datetime, before_datetime)
|
# We want future and ongoing events, so we use ends_on
|
||||||
|
# See issue #1567
|
||||||
|
|> event_filter_ends_on(after_datetime, before_datetime)
|
||||||
|> event_order_by(order_by, order_direction)
|
|> event_order_by(order_by, order_direction)
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_drafts_for_user(integer, integer | nil, integer | nil) :: Page.t(Event.t())
|
@spec list_drafts_for_user(integer, integer | nil, integer | nil) :: Page.t(Event.t())
|
||||||
|
@ -529,6 +546,7 @@ defmodule Mobilizon.Events do
|
||||||
|> filter_draft(true)
|
|> filter_draft(true)
|
||||||
|> order_by(desc: :updated_at)
|
|> order_by(desc: :updated_at)
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean
|
@spec user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean
|
||||||
|
@ -552,6 +570,7 @@ defmodule Mobilizon.Events do
|
||||||
|> close_events_query(radius)
|
|> close_events_query(radius)
|
||||||
|> filter_draft()
|
|> filter_draft()
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -587,7 +606,7 @@ defmodule Mobilizon.Events do
|
||||||
|> events_for_search_query()
|
|> events_for_search_query()
|
||||||
|> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now()))
|
|> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now()))
|
||||||
|> events_for_ends_on(Map.get(args, :ends_on))
|
|> events_for_ends_on(Map.get(args, :ends_on))
|
||||||
|> events_for_longevents(Map.get(args, :longevents))
|
|> events_for_long_events(Map.get(args, :long_events))
|
||||||
|> events_for_category(args)
|
|> events_for_category(args)
|
||||||
|> events_for_categories(args)
|
|> events_for_categories(args)
|
||||||
|> events_for_languages(args)
|
|> events_for_languages(args)
|
||||||
|
@ -602,7 +621,8 @@ defmodule Mobilizon.Events do
|
||||||
|> filter_local_or_from_followed_instances_events()
|
|> filter_local_or_from_followed_instances_events()
|
||||||
|> filter_public_visibility()
|
|> filter_public_visibility()
|
||||||
|> event_order(Map.get(args, :sort_by, :match_desc), search_string)
|
|> event_order(Map.get(args, :sort_by, :match_desc), search_string)
|
||||||
|> Page.build_page(page, limit, :begins_on)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -978,6 +998,7 @@ defmodule Mobilizon.Events do
|
||||||
actor_id
|
actor_id
|
||||||
|> event_participations_for_actor_query()
|
|> event_participations_for_actor_query()
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -993,6 +1014,7 @@ defmodule Mobilizon.Events do
|
||||||
actor_id
|
actor_id
|
||||||
|> event_participations_for_actor_query(DateTime.utc_now())
|
|> event_participations_for_actor_query(DateTime.utc_now())
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -1394,14 +1416,14 @@ defmodule Mobilizon.Events do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec events_for_longevents(Ecto.Queryable.t(), Boolean.t() | nil) :: Ecto.Query.t()
|
@spec events_for_long_events(Ecto.Queryable.t(), Boolean.t() | nil) :: Ecto.Query.t()
|
||||||
defp events_for_longevents(query, longevents) do
|
defp events_for_long_events(query, long_events) do
|
||||||
duration = Config.get([:instance, :duration_of_long_event], 0)
|
duration = Config.get([:instance, :duration_of_long_event], 0)
|
||||||
|
|
||||||
if duration <= 0 do
|
if duration <= 0 do
|
||||||
query
|
query
|
||||||
else
|
else
|
||||||
case longevents do
|
case long_events do
|
||||||
nil ->
|
nil ->
|
||||||
query
|
query
|
||||||
|
|
||||||
|
@ -1871,6 +1893,7 @@ defmodule Mobilizon.Events do
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|> with_virtual_fields()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_participations_for_user_query(integer()) :: Ecto.Query.t()
|
@spec list_participations_for_user_query(integer()) :: Ecto.Query.t()
|
||||||
|
@ -2003,6 +2026,35 @@ defmodule Mobilizon.Events do
|
||||||
|> where([e], e.begins_on > ^after_datetime)
|
|> where([e], e.begins_on > ^after_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp event_filter_ends_on(query, nil, nil), do: query
|
||||||
|
|
||||||
|
defp event_filter_ends_on(query, %DateTime{} = after_datetime, nil) do
|
||||||
|
where(
|
||||||
|
query,
|
||||||
|
[e],
|
||||||
|
(is_nil(e.ends_on) and e.begins_on >= ^after_datetime) or
|
||||||
|
e.ends_on >= ^after_datetime
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp event_filter_ends_on(query, nil, %DateTime{} = before_datetime) do
|
||||||
|
where(
|
||||||
|
query,
|
||||||
|
[e],
|
||||||
|
(is_nil(e.ends_on) and e.begins_on <= ^before_datetime) or e.ends_on <= ^before_datetime
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp event_filter_ends_on(
|
||||||
|
query,
|
||||||
|
%DateTime{} = after_datetime,
|
||||||
|
%DateTime{} = before_datetime
|
||||||
|
) do
|
||||||
|
query
|
||||||
|
|> event_filter_ends_on(after_datetime, nil)
|
||||||
|
|> event_filter_ends_on(nil, before_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
defp event_order_by(query, order_by, direction)
|
defp event_order_by(query, order_by, direction)
|
||||||
when order_by in [:begins_on, :inserted_at, :updated_at] and direction in [:asc, :desc] do
|
when order_by in [:begins_on, :inserted_at, :updated_at] and direction in [:asc, :desc] do
|
||||||
order_by_instruction = Keyword.new([{direction, order_by}])
|
order_by_instruction = Keyword.new([{direction, order_by}])
|
||||||
|
@ -2098,4 +2150,44 @@ defmodule Mobilizon.Events do
|
||||||
|> preload_for_event()
|
|> preload_for_event()
|
||||||
|> Page.chunk(chunk_size)
|
|> Page.chunk(chunk_size)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handling the case where Repo.XXXX() return nil
|
||||||
|
def with_virtual_fields(nil), do: nil
|
||||||
|
|
||||||
|
# if envent has no end date, it can not be a long events
|
||||||
|
def with_virtual_fields(%Event{} = event) when is_nil(event.ends_on),
|
||||||
|
do: %{event | long_event: false}
|
||||||
|
|
||||||
|
# Handling the case where there is an event
|
||||||
|
# Using Repo.one(), for example
|
||||||
|
def with_virtual_fields(%Event{} = event) do
|
||||||
|
duration = Config.get([:instance, :duration_of_long_event], 0)
|
||||||
|
|
||||||
|
event_duration = DateTime.diff(event.ends_on, event.begins_on, :day)
|
||||||
|
|
||||||
|
# duration need to be > 0 for long event to be activated
|
||||||
|
long_event = duration > 0 && event_duration > duration
|
||||||
|
|
||||||
|
%{event | long_event: long_event}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handling the case where there is a list of events
|
||||||
|
# Using Repo.all(), for example
|
||||||
|
def with_virtual_fields(events) when is_list(events) do
|
||||||
|
Enum.map(events, &with_virtual_fields/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handling the case of a paginated list of events
|
||||||
|
def with_virtual_fields(%Page{total: _total, elements: elements} = page) do
|
||||||
|
elements_with_virtual_fields = Enum.map(elements, &with_virtual_fields/1)
|
||||||
|
%{page | elements: elements_with_virtual_fields}
|
||||||
|
end
|
||||||
|
|
||||||
|
# In case the function is called on an element without virtual_fields
|
||||||
|
def with_virtual_fields(invalid) do
|
||||||
|
Logger.warning("with_virtual_fields called on invalid element : #{inspect(invalid)}")
|
||||||
|
|
||||||
|
# Return the element without modification
|
||||||
|
invalid
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -73,7 +73,7 @@ defmodule Mobilizon.Instances do
|
||||||
query
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
%Page{elements: elements} = paged_instances = Page.build_page(query, page, limit, :domain)
|
%Page{elements: elements} = paged_instances = Page.build_page(query, page, limit)
|
||||||
|
|
||||||
%Page{
|
%Page{
|
||||||
paged_instances
|
paged_instances
|
||||||
|
|
|
@ -19,15 +19,23 @@ defmodule Mobilizon.Storage.Page do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a Page struct for a query.
|
Returns a Page struct for a query.
|
||||||
|
|
||||||
`field` is used to define the field that will be used for the count aggregate, which should be the same as the field used for order_by
|
|
||||||
See https://stackoverflow.com/q/12693089/10204399
|
|
||||||
"""
|
"""
|
||||||
@spec build_page(Ecto.Queryable.t(), integer | nil, integer | nil, atom()) :: t(any)
|
@spec build_page(Ecto.Queryable.t(), integer | nil, integer) :: t(any)
|
||||||
def build_page(query, page, limit, field \\ :id) do
|
def build_page(query, page, limit) do
|
||||||
|
count_query =
|
||||||
|
query
|
||||||
|
# Exclude select because we add a new one below
|
||||||
|
|> exclude(:select)
|
||||||
|
# Exclude order_by for perf
|
||||||
|
|> exclude(:order_by)
|
||||||
|
# Exclude preloads to avoid error "cannot preload associations in subquery"
|
||||||
|
|> exclude(:preload)
|
||||||
|
|> subquery()
|
||||||
|
|> select([r], count(fragment("*")))
|
||||||
|
|
||||||
[total, elements] =
|
[total, elements] =
|
||||||
[
|
[
|
||||||
fn -> Repo.aggregate(query, :count, field) end,
|
fn -> Repo.one(count_query) end,
|
||||||
fn -> Repo.all(paginate(query, page, limit)) end
|
fn -> Repo.all(paginate(query, page, limit)) end
|
||||||
]
|
]
|
||||||
|> Enum.map(&Task.async/1)
|
|> Enum.map(&Task.async/1)
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -1,7 +1,7 @@
|
||||||
defmodule Mobilizon.Mixfile do
|
defmodule Mobilizon.Mixfile do
|
||||||
use Mix.Project
|
use Mix.Project
|
||||||
|
|
||||||
@version "5.0.0-beta.1"
|
@version "5.1.0"
|
||||||
|
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -1,12 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "mobilizon",
|
"name": "mobilizon",
|
||||||
"version": "5.0.0-beta.1",
|
"version": "5.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mobilizon",
|
"name": "mobilizon",
|
||||||
"version": "5.0.0-beta.1",
|
"version": "5.1.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.5",
|
"@apollo/client": "^3.9.5",
|
||||||
"@framasoft/socket": "^1.0.0",
|
"@framasoft/socket": "^1.0.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mobilizon",
|
"name": "mobilizon",
|
||||||
"version": "5.0.0-beta.1",
|
"version": "5.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
defmodule Mobilizon.Storage.Repo.Migrations.AddEventPhysicalAddressIndex do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
create(index("events", [:physical_address_id], name: :events_phys_addr_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop(index("events", [:physical_address_id], name: :events_phys_addr_id))
|
||||||
|
end
|
||||||
|
end
|
|
@ -2268,7 +2268,7 @@ type RootQueryType {
|
||||||
endsOn: DateTime
|
endsOn: DateTime
|
||||||
|
|
||||||
"Filter for long events in function of configuration parameter 'duration_of_long_event'"
|
"Filter for long events in function of configuration parameter 'duration_of_long_event'"
|
||||||
longevents: Boolean
|
longEvents: Boolean
|
||||||
): Events
|
): Events
|
||||||
|
|
||||||
"Interact with an URI"
|
"Interact with an URI"
|
||||||
|
|
|
@ -210,7 +210,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-check {
|
.checkbox-check {
|
||||||
@apply appearance-none bg-primary border-primary;
|
@apply appearance-none border-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-checked {
|
.checkbox-checked {
|
||||||
|
@ -250,7 +250,7 @@ body {
|
||||||
@apply mr-2;
|
@apply mr-2;
|
||||||
}
|
}
|
||||||
.form-radio {
|
.form-radio {
|
||||||
@apply bg-none text-primary accent-primary;
|
@apply bg-none text-primary border-primary accent-primary;
|
||||||
}
|
}
|
||||||
.radio-label {
|
.radio-label {
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
|
|
|
@ -110,67 +110,66 @@
|
||||||
{{ t("Actions") }}
|
{{ t("Actions") }}
|
||||||
</o-button>
|
</o-button>
|
||||||
</template>
|
</template>
|
||||||
<o-dropdown-item aria-role="listitem" has-link v-if="canManageEvent">
|
<o-dropdown-item
|
||||||
<router-link
|
aria-role="listitem"
|
||||||
class="flex gap-1"
|
has-link
|
||||||
:to="{
|
v-if="canManageEvent"
|
||||||
|
@click="
|
||||||
|
router.push({
|
||||||
name: RouteName.PARTICIPATIONS,
|
name: RouteName.PARTICIPATIONS,
|
||||||
params: { eventId: event?.uuid },
|
params: { eventId: event?.uuid },
|
||||||
}"
|
})
|
||||||
>
|
"
|
||||||
<AccountMultiple />
|
>
|
||||||
{{ t("Participations") }}
|
<AccountMultiple />
|
||||||
</router-link>
|
{{ t("Participations") }}
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
<o-dropdown-item aria-role="listitem" has-link v-if="canManageEvent">
|
<o-dropdown-item
|
||||||
<router-link
|
aria-role="listitem"
|
||||||
class="flex gap-1"
|
has-link
|
||||||
:to="{
|
v-if="canManageEvent"
|
||||||
|
@click="
|
||||||
|
router.push({
|
||||||
name: RouteName.ANNOUNCEMENTS,
|
name: RouteName.ANNOUNCEMENTS,
|
||||||
params: { eventId: event?.uuid },
|
params: { eventId: event?.uuid },
|
||||||
}"
|
})
|
||||||
>
|
"
|
||||||
<Bullhorn />
|
>
|
||||||
{{ t("Announcements") }}
|
<Bullhorn />
|
||||||
</router-link>
|
{{ t("Announcements") }}
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
<o-dropdown-item
|
<o-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
has-link
|
has-link
|
||||||
v-if="canManageEvent || event?.draft"
|
v-if="canManageEvent || event?.draft"
|
||||||
>
|
@click="
|
||||||
<router-link
|
router.push({
|
||||||
class="flex gap-1"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.EDIT_EVENT,
|
name: RouteName.EDIT_EVENT,
|
||||||
params: { eventId: event?.uuid },
|
params: { eventId: event?.uuid },
|
||||||
}"
|
})
|
||||||
>
|
"
|
||||||
<Pencil />
|
>
|
||||||
{{ t("Edit") }}
|
<Pencil />
|
||||||
</router-link>
|
{{ t("Edit") }}
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
<o-dropdown-item
|
<o-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
has-link
|
has-link
|
||||||
v-if="canManageEvent || event?.draft"
|
v-if="canManageEvent || event?.draft"
|
||||||
>
|
@click="
|
||||||
<router-link
|
router.push({
|
||||||
class="flex gap-1"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.DUPLICATE_EVENT,
|
name: RouteName.DUPLICATE_EVENT,
|
||||||
params: { eventId: event?.uuid },
|
params: { eventId: event?.uuid },
|
||||||
}"
|
})
|
||||||
>
|
"
|
||||||
<ContentDuplicate />
|
>
|
||||||
{{ t("Duplicate") }}
|
<ContentDuplicate />
|
||||||
</router-link>
|
{{ t("Duplicate") }}
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
<o-dropdown-item
|
<o-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="canManageEvent || event?.draft"
|
v-if="canManageEvent || event?.draft"
|
||||||
@click="openDeleteEventModal"
|
@click="openDeleteEventModal"
|
||||||
@keyup.enter="openDeleteEventModal"
|
|
||||||
><span class="flex gap-1">
|
><span class="flex gap-1">
|
||||||
<Delete />
|
<Delete />
|
||||||
{{ t("Delete") }}
|
{{ t("Delete") }}
|
||||||
|
|
66
src/components/Event/EventDatePicker.vue
Normal file
66
src/components/Event/EventDatePicker.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
:key is required to force rerender when time change
|
||||||
|
If not used, the input becomes empty
|
||||||
|
See : https://vuejs.org/api/built-in-special-attributes.html#key
|
||||||
|
-->
|
||||||
|
<input
|
||||||
|
:type="time ? 'datetime-local' : 'date'"
|
||||||
|
:key="time.toString()"
|
||||||
|
class="rounded invalid:border-red-500 dark:bg-zinc-600"
|
||||||
|
v-model="component"
|
||||||
|
:min="computeMin"
|
||||||
|
@blur="$emit('blur')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: Date | null;
|
||||||
|
time: boolean;
|
||||||
|
min?: Date | null | undefined;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
min: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "blur"]);
|
||||||
|
|
||||||
|
/** Format a Date to 'YYYY-MM-DDTHH:MM' based on local time zone */
|
||||||
|
const UTCToLocal = (date: Date) => {
|
||||||
|
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
||||||
|
return localDate.toISOString().slice(0, props.time ? 16 : 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const component = computed({
|
||||||
|
get() {
|
||||||
|
if (!props.modelValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return UTCToLocal(props.modelValue);
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (!value) {
|
||||||
|
emit("update:modelValue", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
if (!props.time) {
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("update:modelValue", date);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const computeMin = computed((): string | undefined => {
|
||||||
|
if (!props.min) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return UTCToLocal(props.min);
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -3,83 +3,74 @@
|
||||||
<span>{{
|
<span>{{
|
||||||
formatDateTimeString(beginsOn, timezoneToShow, showStartTime)
|
formatDateTimeString(beginsOn, timezoneToShow, showStartTime)
|
||||||
}}</span>
|
}}</span>
|
||||||
<br />
|
|
||||||
<o-switch
|
|
||||||
size="small"
|
|
||||||
v-model="showLocalTimezone"
|
|
||||||
v-if="differentFromUserTimezone"
|
|
||||||
>
|
|
||||||
{{ singleTimeZone }}
|
|
||||||
</o-switch>
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="isSameDay() && showStartTime && showEndTime">
|
<!-- endsOn is set and isSameDay() -->
|
||||||
<span>{{
|
<template v-else-if="isSameDay()">
|
||||||
t("On {date} from {startTime} to {endTime}", {
|
<p v-if="showStartTime && showEndTime">
|
||||||
date: formatDate(beginsOn),
|
<span>{{
|
||||||
startTime: formatTime(beginsOn, timezoneToShow),
|
t("On {date} from {startTime} to {endTime}", {
|
||||||
endTime: formatTime(endsOn, timezoneToShow),
|
date: formatDate(beginsOn),
|
||||||
})
|
startTime: formatTime(beginsOn),
|
||||||
}}</span>
|
endTime: formatTime(endsOn),
|
||||||
<br />
|
})
|
||||||
<o-switch
|
}}</span>
|
||||||
size="small"
|
</p>
|
||||||
v-model="showLocalTimezone"
|
<p v-else-if="showStartTime && !showEndTime">
|
||||||
v-if="differentFromUserTimezone"
|
{{
|
||||||
>
|
t("On {date} starting at {startTime}", {
|
||||||
{{ singleTimeZone }}
|
date: formatDate(beginsOn),
|
||||||
</o-switch>
|
startTime: formatTime(beginsOn),
|
||||||
</p>
|
})
|
||||||
<p v-else-if="isSameDay() && showStartTime && !showEndTime">
|
}}
|
||||||
{{
|
</p>
|
||||||
t("On {date} starting at {startTime}", {
|
<p v-else-if="!showStartTime && showEndTime">
|
||||||
date: formatDate(beginsOn),
|
{{
|
||||||
startTime: formatTime(beginsOn, timezoneToShow),
|
t("On {date} ending at {endTime}", {
|
||||||
})
|
date: formatDate(beginsOn),
|
||||||
}}
|
endTime: formatTime(endsOn),
|
||||||
</p>
|
})
|
||||||
<p v-else-if="isSameDay()">
|
}}
|
||||||
{{ t("On {date}", { date: formatDate(beginsOn) }) }}
|
</p>
|
||||||
</p>
|
<p v-else>
|
||||||
<p v-else-if="endsOn && showStartTime && showEndTime">
|
{{ t("On {date}", { date: formatDate(beginsOn) }) }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<!-- endsOn is set and !isSameDay() -->
|
||||||
|
<p v-else-if="showStartTime && showEndTime">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
||||||
startDate: formatDate(beginsOn),
|
startDate: formatDate(beginsOn),
|
||||||
startTime: formatTime(beginsOn, timezoneToShow),
|
startTime: formatTime(beginsOn),
|
||||||
endDate: formatDate(endsOn),
|
endDate: formatDate(endsOn),
|
||||||
endTime: formatTime(endsOn, timezoneToShow),
|
endTime: formatTime(endsOn),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
|
||||||
<o-switch
|
|
||||||
size="small"
|
|
||||||
v-model="showLocalTimezone"
|
|
||||||
v-if="differentFromUserTimezone"
|
|
||||||
>
|
|
||||||
{{ multipleTimeZones }}
|
|
||||||
</o-switch>
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="endsOn && showStartTime">
|
<p v-else-if="showStartTime && !showEndTime">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
t("From the {startDate} at {startTime} to the {endDate}", {
|
t("From the {startDate} at {startTime} to the {endDate}", {
|
||||||
startDate: formatDate(beginsOn),
|
startDate: formatDate(beginsOn),
|
||||||
startTime: formatTime(beginsOn, timezoneToShow),
|
startTime: formatTime(beginsOn),
|
||||||
endDate: formatDate(endsOn),
|
endDate: formatDate(endsOn),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
|
||||||
<o-switch
|
|
||||||
size="small"
|
|
||||||
v-model="showLocalTimezone"
|
|
||||||
v-if="differentFromUserTimezone"
|
|
||||||
>
|
|
||||||
{{ singleTimeZone }}
|
|
||||||
</o-switch>
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="endsOn">
|
<p v-else-if="!showStartTime && showEndTime">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
t("From the {startDate} to the {endDate} at {endTime}", {
|
||||||
|
startDate: formatDate(beginsOn),
|
||||||
|
endDate: formatDate(endsOn),
|
||||||
|
endTime: formatTime(endsOn),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
{{
|
{{
|
||||||
t("From the {startDate} to the {endDate}", {
|
t("From the {startDate} to the {endDate}", {
|
||||||
startDate: formatDate(beginsOn),
|
startDate: formatDate(beginsOn),
|
||||||
|
@ -87,6 +78,13 @@
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
|
<o-switch
|
||||||
|
size="small"
|
||||||
|
v-model="showLocalTimezone"
|
||||||
|
v-if="differentFromUserTimezone"
|
||||||
|
>
|
||||||
|
{{ singleTimeZone }}
|
||||||
|
</o-switch>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
|
@ -119,7 +117,7 @@ const showLocalTimezone = ref(false);
|
||||||
|
|
||||||
const timezoneToShow = computed((): string | undefined => {
|
const timezoneToShow = computed((): string | undefined => {
|
||||||
if (showLocalTimezone.value) {
|
if (showLocalTimezone.value) {
|
||||||
return props.timezone;
|
return props.timezone ?? userActualTimezone.value;
|
||||||
}
|
}
|
||||||
return userActualTimezone.value;
|
return userActualTimezone.value;
|
||||||
});
|
});
|
||||||
|
@ -132,33 +130,43 @@ const userActualTimezone = computed((): string => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (value: string): string | undefined => {
|
const formatDate = (value: string): string | undefined => {
|
||||||
return formatDateString(value);
|
return formatDateString(value, timezoneToShow.value ?? "Etc/UTC");
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (
|
const formatTime = (value: string): string | undefined => {
|
||||||
value: string,
|
return formatTimeString(value, timezoneToShow.value ?? "Etc/UTC");
|
||||||
timezone: string | undefined = undefined
|
|
||||||
): string | undefined => {
|
|
||||||
return formatTimeString(value, timezone ?? "Etc/UTC");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We need to compare date after the offset is applied
|
||||||
|
// Because some date can be in the same day in a time zone, but different day in another.
|
||||||
|
// Example : From 2025-11-30 at 23:00 to 2025-12-01 01:00 in Asia/Shanghai (different days)
|
||||||
|
// It is from 2025-11-30 at 16:00 to 2025-11-30 at 18:00 in Europe/Paris (same day)
|
||||||
const isSameDay = (): boolean => {
|
const isSameDay = (): boolean => {
|
||||||
if (!props.endsOn) return false;
|
if (!props.endsOn) return false;
|
||||||
|
|
||||||
|
const offset =
|
||||||
|
getTimezoneOffset(timezoneToShow.value ?? "Etc/UTC", new Date()) /
|
||||||
|
(60 * 1000);
|
||||||
|
|
||||||
|
const beginsOnOffset = new Date(props.beginsOn);
|
||||||
|
beginsOnOffset.setUTCMinutes(beginsOnOffset.getUTCMinutes() + offset);
|
||||||
|
|
||||||
|
const endsOnOffset = new Date(props.endsOn);
|
||||||
|
endsOnOffset.setUTCMinutes(endsOnOffset.getUTCMinutes() + offset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
beginsOnDate.value.toDateString() === new Date(props.endsOn).toDateString()
|
beginsOnOffset.getUTCFullYear() === endsOnOffset.getUTCFullYear() &&
|
||||||
|
beginsOnOffset.getUTCMonth() === endsOnOffset.getUTCMonth() &&
|
||||||
|
beginsOnOffset.getUTCDate() === endsOnOffset.getUTCDate()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const beginsOnDate = computed((): Date => {
|
|
||||||
return new Date(props.beginsOn);
|
|
||||||
});
|
|
||||||
|
|
||||||
const differentFromUserTimezone = computed((): boolean => {
|
const differentFromUserTimezone = computed((): boolean => {
|
||||||
return (
|
return (
|
||||||
!!props.timezone &&
|
!!props.timezone &&
|
||||||
!!userActualTimezone.value &&
|
!!userActualTimezone.value &&
|
||||||
getTimezoneOffset(props.timezone, beginsOnDate.value) !==
|
getTimezoneOffset(props.timezone, new Date()) !==
|
||||||
getTimezoneOffset(userActualTimezone.value, beginsOnDate.value) &&
|
getTimezoneOffset(userActualTimezone.value, new Date()) &&
|
||||||
props.timezone !== userActualTimezone.value
|
props.timezone !== userActualTimezone.value
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -173,15 +181,4 @@ const singleTimeZone = computed((): string => {
|
||||||
timezone: timezoneToShow.value,
|
timezone: timezoneToShow.value,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const multipleTimeZones = computed((): string => {
|
|
||||||
if (showLocalTimezone.value) {
|
|
||||||
return t("Local times ({timezone})", {
|
|
||||||
timezone: timezoneToShow.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return t("Times in your timezone ({timezone})", {
|
|
||||||
timezone: timezoneToShow.value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -124,9 +124,13 @@
|
||||||
<!-- Less than 10 seats left -->
|
<!-- Less than 10 seats left -->
|
||||||
<span class="has-text-danger" v-if="lastSeatsLeft">
|
<span class="has-text-danger" v-if="lastSeatsLeft">
|
||||||
{{
|
{{
|
||||||
t("{number} seats left", {
|
t(
|
||||||
number: seatsLeft,
|
"{number} seats left",
|
||||||
})
|
{
|
||||||
|
number: seatsLeft,
|
||||||
|
},
|
||||||
|
seatsLeft ?? 0
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -202,16 +206,14 @@
|
||||||
ParticipantRole.NOT_APPROVED,
|
ParticipantRole.NOT_APPROVED,
|
||||||
].includes(participation.role)
|
].includes(participation.role)
|
||||||
"
|
"
|
||||||
|
@click="
|
||||||
|
gotToWithCheck(participation, {
|
||||||
|
name: RouteName.EDIT_EVENT,
|
||||||
|
params: { eventId: participation.event.uuid },
|
||||||
|
})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex gap-1">
|
||||||
class="flex gap-1"
|
|
||||||
@click="
|
|
||||||
gotToWithCheck(participation, {
|
|
||||||
name: RouteName.EDIT_EVENT,
|
|
||||||
params: { eventId: participation.event.uuid },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Pencil />
|
<Pencil />
|
||||||
{{ t("Edit") }}
|
{{ t("Edit") }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -220,16 +222,14 @@
|
||||||
<o-dropdown-item
|
<o-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="participation.role === ParticipantRole.CREATOR"
|
v-if="participation.role === ParticipantRole.CREATOR"
|
||||||
|
@click="
|
||||||
|
gotToWithCheck(participation, {
|
||||||
|
name: RouteName.DUPLICATE_EVENT,
|
||||||
|
params: { eventId: participation.event.uuid },
|
||||||
|
})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex gap-1">
|
||||||
class="flex gap-1"
|
|
||||||
@click="
|
|
||||||
gotToWithCheck(participation, {
|
|
||||||
name: RouteName.DUPLICATE_EVENT,
|
|
||||||
params: { eventId: participation.event.uuid },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ContentDuplicate />
|
<ContentDuplicate />
|
||||||
{{ t("Duplicate") }}
|
{{ t("Duplicate") }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -243,8 +243,9 @@
|
||||||
ParticipantRole.NOT_APPROVED,
|
ParticipantRole.NOT_APPROVED,
|
||||||
].includes(participation.role)
|
].includes(participation.role)
|
||||||
"
|
"
|
||||||
|
@click="openDeleteEventModalWrapper"
|
||||||
>
|
>
|
||||||
<div @click="openDeleteEventModalWrapper" class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Delete />
|
<Delete />
|
||||||
{{ t("Delete") }}
|
{{ t("Delete") }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,16 +259,14 @@
|
||||||
ParticipantRole.NOT_APPROVED,
|
ParticipantRole.NOT_APPROVED,
|
||||||
].includes(participation.role)
|
].includes(participation.role)
|
||||||
"
|
"
|
||||||
|
@click="
|
||||||
|
gotToWithCheck(participation, {
|
||||||
|
name: RouteName.PARTICIPATIONS,
|
||||||
|
params: { eventId: participation.event.uuid },
|
||||||
|
})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex gap-1">
|
||||||
class="flex gap-1"
|
|
||||||
@click="
|
|
||||||
gotToWithCheck(participation, {
|
|
||||||
name: RouteName.PARTICIPATIONS,
|
|
||||||
params: { eventId: participation.event.uuid },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<AccountMultiplePlus />
|
<AccountMultiplePlus />
|
||||||
{{ t("Manage participations") }}
|
{{ t("Manage participations") }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -282,30 +281,28 @@
|
||||||
ParticipantRole.NOT_APPROVED,
|
ParticipantRole.NOT_APPROVED,
|
||||||
].includes(participation.role)
|
].includes(participation.role)
|
||||||
"
|
"
|
||||||
>
|
@click="
|
||||||
<router-link
|
router.push({
|
||||||
class="flex gap-1"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.ANNOUNCEMENTS,
|
name: RouteName.ANNOUNCEMENTS,
|
||||||
params: { eventId: participation.event?.uuid },
|
params: { eventId: participation.event?.uuid },
|
||||||
}"
|
})
|
||||||
>
|
"
|
||||||
<Bullhorn />
|
>
|
||||||
{{ t("Announcements") }}
|
<Bullhorn />
|
||||||
</router-link>
|
{{ t("Announcements") }}
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
|
|
||||||
<o-dropdown-item aria-role="listitem">
|
<o-dropdown-item
|
||||||
<router-link
|
aria-role="listitem"
|
||||||
class="flex gap-1"
|
@click="
|
||||||
:to="{
|
router.push({
|
||||||
name: RouteName.EVENT,
|
name: RouteName.EVENT,
|
||||||
params: { uuid: participation.event.uuid },
|
params: { eventId: participation.event.uuid },
|
||||||
}"
|
})
|
||||||
>
|
"
|
||||||
<ViewCompact />
|
>
|
||||||
{{ t("View event page") }}
|
<ViewCompact />
|
||||||
</router-link>
|
{{ t("View event page") }}
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
</o-dropdown>
|
</o-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</o-autocomplete>
|
</o-autocomplete>
|
||||||
|
<slot></slot>
|
||||||
<o-button
|
<o-button
|
||||||
:disabled="!selected"
|
:disabled="!selected"
|
||||||
@click="resetAddress"
|
@click="resetAddress"
|
||||||
|
@ -381,7 +382,7 @@ const asyncData = async (query: string): Promise<void> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedAddressText = computed(() => {
|
const selectedAddressText = computed(() => {
|
||||||
if (!selected) return undefined;
|
if (!selected || !selected.id) return undefined;
|
||||||
return addressFullName(selected);
|
return addressFullName(selected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="max-w-md mx-auto">
|
<div class="max-w-md mx-auto">
|
||||||
<o-input
|
<o-input
|
||||||
|
expanded
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:placeholder="t('Filter by profile or group name')"
|
:placeholder="t('Filter by profile or group name')"
|
||||||
v-model="actorFilterProxy"
|
v-model="actorFilterProxy"
|
||||||
|
|
|
@ -63,9 +63,10 @@
|
||||||
<h2 class="">{{ $t("Pick a profile or a group") }}</h2>
|
<h2 class="">{{ $t("Pick a profile or a group") }}</h2>
|
||||||
</header>
|
</header>
|
||||||
<section class="">
|
<section class="">
|
||||||
<div class="flex flex-wrap gap-2 items-center">
|
<div class="flex flex-wrap gap-2 items-center flex-col lg:flex-row">
|
||||||
<div class="max-h-[400px] overflow-y-auto flex-1">
|
<div class="max-h-[400px] overflow-y-auto flex-1 w-full">
|
||||||
<organizer-picker
|
<organizer-picker
|
||||||
|
class="p-5 w-3/4"
|
||||||
v-if="currentActor"
|
v-if="currentActor"
|
||||||
:current-actor="currentActor"
|
:current-actor="currentActor"
|
||||||
:identities="identities ?? []"
|
:identities="identities ?? []"
|
||||||
|
@ -80,6 +81,7 @@
|
||||||
<div v-if="isSelectedActorAGroup">
|
<div v-if="isSelectedActorAGroup">
|
||||||
<p>{{ $t("Add a contact") }}</p>
|
<p>{{ $t("Add a contact") }}</p>
|
||||||
<o-input
|
<o-input
|
||||||
|
expanded
|
||||||
:placeholder="$t('Filter by name')"
|
:placeholder="$t('Filter by name')"
|
||||||
:value="contactFilter"
|
:value="contactFilter"
|
||||||
@input="debounceSetFilterByName"
|
@input="debounceSetFilterByName"
|
||||||
|
@ -137,8 +139,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="my-2">
|
<footer class="my-2 text-center sm:text-right">
|
||||||
<o-button variant="primary" @click="pickActor">
|
<o-button
|
||||||
|
variant="primary"
|
||||||
|
class="w-full sm:w-auto"
|
||||||
|
@click="pickActor"
|
||||||
|
>
|
||||||
{{ $t("Pick") }}
|
{{ $t("Pick") }}
|
||||||
</o-button>
|
</o-button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -12,30 +12,24 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { formatTimeString } from "@/filters/datetime";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
import Clock from "vue-material-design-icons/ClockTimeTenOutline.vue";
|
import Clock from "vue-material-design-icons/ClockTimeTenOutline.vue";
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: "global" });
|
|
||||||
|
|
||||||
const localeConverted = locale.replace("_", "-");
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
date: string;
|
date: string;
|
||||||
|
timezone?: string;
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{ small: false }
|
{ small: false, timezone: "Etc/UTC" }
|
||||||
);
|
);
|
||||||
|
|
||||||
const dateObj = computed<Date>(() => new Date(props.date));
|
const dateObj = computed<Date>(() => new Date(props.date));
|
||||||
|
|
||||||
const time = computed<string>(() =>
|
const time = computed<string>(() =>
|
||||||
dateObj.value.toLocaleTimeString(localeConverted, {
|
formatTimeString(props.date, props.timezone)
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const smallStyle = computed<string>(() => (props.small ? "0.9" : "2"));
|
const smallStyle = computed<string>(() => (props.small ? "0.9" : "2"));
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section class="flex flex-col border-2 border-yellow-1 rounded-lg">
|
||||||
class="flex flex-col mb-3 border-2"
|
|
||||||
:class="{
|
|
||||||
'border-mbz-purple': privateSection,
|
|
||||||
'border-yellow-1': !privateSection,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title">
|
<div class="flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title">
|
||||||
<div class="flex flex-1 gap-1">
|
<div class="flex flex-1 gap-1">
|
||||||
<o-icon :icon="icon" custom-size="36" />
|
<o-icon :icon="icon" custom-size="36" />
|
||||||
<h2 class="text-2xl font-medium mt-0">{{ title }}</h2>
|
<h2 class="text-2xl font-medium mt-0">{{ title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<router-link class="self-center" :to="route">{{
|
<router-link v-if="route" class="self-center" :to="route">{{
|
||||||
t("View all")
|
t("View all")
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1 min-h-40">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end p-2">
|
<div class="flex flex-wrap justify-end p-2">
|
||||||
<slot name="create"></slot>
|
<slot name="create"></slot>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -31,10 +25,9 @@ withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
privateSection?: boolean;
|
|
||||||
route: { name: string; params: { preferredUsername: string } };
|
route: { name: string; params: { preferredUsername: string } };
|
||||||
}>(),
|
}>(),
|
||||||
{ privateSection: true }
|
{ route: undefined }
|
||||||
);
|
);
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<group-section
|
<group-section
|
||||||
:title="t('Events')"
|
:title="longEvent ? t('Activities') : t('Events')"
|
||||||
icon="calendar"
|
icon="calendar"
|
||||||
:privateSection="false"
|
|
||||||
:route="{
|
:route="{
|
||||||
name: RouteName.GROUP_EVENTS,
|
name: RouteName.GROUP_EVENTS,
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
|
@ -10,21 +9,40 @@
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap gap-2 py-1"
|
class="flex flex-wrap gap-2 p-2"
|
||||||
v-if="group && group.organizedEvents.total > 0"
|
v-if="group && group.organizedEvents.total > 0"
|
||||||
>
|
>
|
||||||
<event-minimalist-card
|
<event-minimalist-card
|
||||||
v-for="event in group.organizedEvents.elements.slice(0, 3)"
|
v-for="event in group.organizedEvents.elements
|
||||||
|
.filter((event) => (longEvent ? event.longEvent : !event.longEvent))
|
||||||
|
.slice(0, 3)"
|
||||||
:event="event"
|
:event="event"
|
||||||
:key="event.uuid"
|
:key="event.uuid"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<empty-content v-else-if="group" icon="calendar" :inline="true">
|
<empty-content v-else-if="group" icon="calendar" :inline="true"
|
||||||
{{ t("No public upcoming events") }}
|
>{{
|
||||||
|
longEvent
|
||||||
|
? t("No public upcoming activities")
|
||||||
|
: t("No public upcoming events")
|
||||||
|
}}
|
||||||
</empty-content>
|
</empty-content>
|
||||||
<!-- <o-skeleton animated v-else></o-skeleton> -->
|
<!-- <o-skeleton animated v-else></o-skeleton> -->
|
||||||
</template>
|
</template>
|
||||||
<template #create>
|
<template #create>
|
||||||
|
<o-button
|
||||||
|
tag="router-link"
|
||||||
|
class="button"
|
||||||
|
variant="text"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.GROUP_EVENTS,
|
||||||
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
|
query: { showPassedEvents: true },
|
||||||
|
}"
|
||||||
|
>{{
|
||||||
|
longEvent ? t("+ View past activities") : t("+ View past events")
|
||||||
|
}}</o-button
|
||||||
|
>
|
||||||
<o-button
|
<o-button
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
v-if="isModerator"
|
v-if="isModerator"
|
||||||
|
@ -33,7 +51,9 @@
|
||||||
query: { actorId: group?.id },
|
query: { actorId: group?.id },
|
||||||
}"
|
}"
|
||||||
class="button is-primary"
|
class="button is-primary"
|
||||||
>{{ t("+ Create an event") }}</o-button
|
>{{
|
||||||
|
longEvent ? t("+ Create an activity") : t("+ Create an event")
|
||||||
|
}}</o-button
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</group-section>
|
</group-section>
|
||||||
|
@ -50,5 +70,9 @@ import GroupSection from "@/components/Group/GroupSection.vue";
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
defineProps<{ group: IGroup; isModerator: boolean }>();
|
defineProps<{
|
||||||
|
group: IGroup;
|
||||||
|
isModerator: boolean;
|
||||||
|
longEvent: boolean;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,16 +2,28 @@
|
||||||
<group-section
|
<group-section
|
||||||
:title="t('Announcements')"
|
:title="t('Announcements')"
|
||||||
icon="bullhorn"
|
icon="bullhorn"
|
||||||
:privateSection="false"
|
|
||||||
:route="{
|
:route="{
|
||||||
name: RouteName.POSTS,
|
name: RouteName.POSTS,
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="p-1">
|
<div class="p-2">
|
||||||
<multi-post-list-item
|
<multi-post-list-item
|
||||||
v-if="group?.posts?.total ?? 0 > 0"
|
v-if="
|
||||||
|
!isMember &&
|
||||||
|
group?.posts.elements.filter(
|
||||||
|
(post) => !post.draft && post.visibility === PostVisibility.PUBLIC
|
||||||
|
).length > 0
|
||||||
|
"
|
||||||
|
:posts="
|
||||||
|
group?.posts.elements.filter(
|
||||||
|
(post) => !post.draft && post.visibility === PostVisibility.PUBLIC
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<multi-post-list-item
|
||||||
|
v-else-if="group?.posts?.total ?? 0 > 0"
|
||||||
:posts="(group?.posts?.elements ?? []).slice(0, 3)"
|
:posts="(group?.posts?.elements ?? []).slice(0, 3)"
|
||||||
:isCurrentActorMember="isMember"
|
:isCurrentActorMember="isMember"
|
||||||
/>
|
/>
|
||||||
|
@ -43,6 +55,7 @@ import { useI18n } from "vue-i18n";
|
||||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||||
import MultiPostListItem from "@/components/Post/MultiPostListItem.vue";
|
import MultiPostListItem from "@/components/Post/MultiPostListItem.vue";
|
||||||
import GroupSection from "@/components/Group/GroupSection.vue";
|
import GroupSection from "@/components/Group/GroupSection.vue";
|
||||||
|
import { PostVisibility } from "@/types/enums";
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
t("Keyword, event title, group name, etc.")
|
t("Keyword, event title, group name, etc.")
|
||||||
}}</label>
|
}}</label>
|
||||||
<o-input
|
<o-input
|
||||||
|
v-if="search != null"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
:placeholder="t('Search')"
|
:placeholder="t('Search')"
|
||||||
id="search_field_input"
|
id="search_field_input"
|
||||||
|
@ -19,57 +20,110 @@
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
expanded
|
expanded
|
||||||
/>
|
/>
|
||||||
<o-button native-type="submit" icon-left="magnify"> </o-button>
|
<full-address-auto-complete
|
||||||
|
:resultType="AddressSearchType.ADMINISTRATIVE"
|
||||||
|
v-model="address"
|
||||||
|
:hide-map="true"
|
||||||
|
:hide-selected="true"
|
||||||
|
:default-text="addressDefaultText"
|
||||||
|
labelClass="sr-only"
|
||||||
|
:placeholder="t('e.g. Nantes, Berlin, Cork, …')"
|
||||||
|
v-on:update:modelValue="modelValueUpdate"
|
||||||
|
>
|
||||||
|
<o-dropdown v-model="distance" position="bottom-right" v-if="distance">
|
||||||
|
<template #trigger="{ active }">
|
||||||
|
<o-button
|
||||||
|
:title="t('Select distance')"
|
||||||
|
:icon-right="active ? 'menu-up' : 'menu-down'"
|
||||||
|
>
|
||||||
|
{{ distanceText }}
|
||||||
|
</o-button>
|
||||||
|
</template>
|
||||||
|
<o-dropdown-item
|
||||||
|
v-for="distance_item in distanceList"
|
||||||
|
:value="distance_item.distance"
|
||||||
|
:label="distance_item.label"
|
||||||
|
:key="distance_item.distance"
|
||||||
|
/>
|
||||||
|
</o-dropdown>
|
||||||
|
</full-address-auto-complete>
|
||||||
|
<o-button
|
||||||
|
class="search-Event min-w-40 mr-1 mb-1"
|
||||||
|
native-type="submit"
|
||||||
|
icon-left="calendar"
|
||||||
|
>
|
||||||
|
{{ t("Events") }}
|
||||||
|
</o-button>
|
||||||
|
<o-button
|
||||||
|
class="search-Activity min-w-40 mr-1 mb-1"
|
||||||
|
native-type="submit"
|
||||||
|
icon-left="calendar-star"
|
||||||
|
v-if="isLongEvents"
|
||||||
|
>
|
||||||
|
{{ t("Activities") }}
|
||||||
|
</o-button>
|
||||||
|
<o-button
|
||||||
|
class="search-Group min-w-40 mr-1 mb-1"
|
||||||
|
native-type="submit"
|
||||||
|
icon-left="account-multiple"
|
||||||
|
>
|
||||||
|
{{ t("Groups") }}
|
||||||
|
</o-button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { IAddress } from "@/types/address.model";
|
import { IAddress } from "@/types/address.model";
|
||||||
import { AddressSearchType } from "@/types/enums";
|
import { AddressSearchType, ContentType } from "@/types/enums";
|
||||||
import {
|
import {
|
||||||
addressToLocation,
|
addressToLocation,
|
||||||
getLocationFromLocal,
|
getAddressFromLocal,
|
||||||
storeLocationInLocal,
|
storeAddressInLocal,
|
||||||
} from "@/utils/location";
|
} from "@/utils/location";
|
||||||
import { computed, defineAsyncComponent } from "vue";
|
import { computed, defineAsyncComponent } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import RouteName from "@/router/name";
|
import RouteName from "@/router/name";
|
||||||
|
import { useIsLongEvents } from "@/composition/apollo/config";
|
||||||
|
|
||||||
defineAsyncComponent(
|
defineAsyncComponent(
|
||||||
() => import("@/components/Event/FullAddressAutoComplete.vue")
|
() => import("@/components/Event/FullAddressAutoComplete.vue")
|
||||||
);
|
);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
location: IAddress | null;
|
address: IAddress | null;
|
||||||
locationDefaultText?: string | null;
|
addressDefaultText?: string | null;
|
||||||
search: string;
|
search: string | null;
|
||||||
|
distance: number | null;
|
||||||
fromLocalStorage?: boolean | false;
|
fromLocalStorage?: boolean | false;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { isLongEvents } = useIsLongEvents();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:location", location: IAddress | null): void;
|
(event: "update:address", address: IAddress | null): void;
|
||||||
(event: "update:search", newSearch: string): void;
|
(event: "update:search", newSearch: string): void;
|
||||||
|
(event: "update:distance", newDistance: number): void;
|
||||||
(event: "submit"): void;
|
(event: "submit"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const location = computed({
|
const address = computed({
|
||||||
get(): IAddress | null {
|
get(): IAddress | null {
|
||||||
if (props.location) {
|
console.debug("-- get address --", props);
|
||||||
return props.location;
|
if (props.address) {
|
||||||
|
return props.address;
|
||||||
}
|
}
|
||||||
if (props.fromLocalStorage) {
|
if (props.fromLocalStorage) {
|
||||||
return getLocationFromLocal();
|
return getAddressFromLocal();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
set(newLocation: IAddress | null) {
|
set(newAddress: IAddress | null) {
|
||||||
emit("update:location", newLocation);
|
emit("update:address", newAddress);
|
||||||
if (props.fromLocalStorage) {
|
if (props.fromLocalStorage) {
|
||||||
storeLocationInLocal(newLocation);
|
storeAddressInLocal(newAddress);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -83,17 +137,79 @@ const search = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = () => {
|
const distance = computed({
|
||||||
|
get(): number {
|
||||||
|
return props.distance;
|
||||||
|
},
|
||||||
|
set(newDistance: number) {
|
||||||
|
emit("update:distance", newDistance);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const distanceText = computed(() => {
|
||||||
|
return distance.value + " km";
|
||||||
|
});
|
||||||
|
|
||||||
|
const distanceList = computed(() => {
|
||||||
|
const distances = [];
|
||||||
|
[5, 10, 25, 50, 100, 150].forEach((value) => {
|
||||||
|
distances.push({
|
||||||
|
distance: value,
|
||||||
|
label: t(
|
||||||
|
"{number} kilometers",
|
||||||
|
{
|
||||||
|
number: value,
|
||||||
|
},
|
||||||
|
value
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return distances;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.debug("initial", distance.value, search.value, address.value);
|
||||||
|
|
||||||
|
const modelValueUpdate = (newaddress: IAddress | null) => {
|
||||||
|
emit("update:address", newaddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = (event) => {
|
||||||
emit("submit");
|
emit("submit");
|
||||||
const { lat, lon } = addressToLocation(location.value);
|
const btn_classes = event.submitter.getAttribute("class").split(" ");
|
||||||
|
const search_query = {
|
||||||
|
locationName: undefined,
|
||||||
|
lat: undefined,
|
||||||
|
lon: undefined,
|
||||||
|
search: undefined,
|
||||||
|
distance: undefined,
|
||||||
|
contentType: undefined,
|
||||||
|
};
|
||||||
|
if (search.value != "") {
|
||||||
|
search_query.search = search.value;
|
||||||
|
}
|
||||||
|
if (address.value) {
|
||||||
|
const { lat, lon } = addressToLocation(address.value);
|
||||||
|
search_query.locationName = address.value.locality ?? address.value.region;
|
||||||
|
search_query.lat = lat;
|
||||||
|
search_query.lon = lon;
|
||||||
|
if (distance.value != null) {
|
||||||
|
search_query.distance = distance.value.toString() + "_km";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (btn_classes.includes("search-Event")) {
|
||||||
|
search_query.contentType = ContentType.EVENTS;
|
||||||
|
}
|
||||||
|
if (btn_classes.includes("search-Activity")) {
|
||||||
|
search_query.contentType = ContentType.LONGEVENTS;
|
||||||
|
}
|
||||||
|
if (btn_classes.includes("search-Group")) {
|
||||||
|
search_query.contentType = ContentType.GROUPS;
|
||||||
|
}
|
||||||
router.push({
|
router.push({
|
||||||
name: RouteName.SEARCH,
|
name: RouteName.SEARCH,
|
||||||
query: {
|
query: {
|
||||||
...route.query,
|
...route.query,
|
||||||
locationName: location.value?.locality ?? location.value?.region,
|
...search_query,
|
||||||
lat,
|
|
||||||
lon,
|
|
||||||
search: search.value,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -97,6 +97,7 @@ import { EventSortField, SortDirection } from "@/types/enums";
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
userLocation: LocationType;
|
userLocation: LocationType;
|
||||||
doingGeoloc?: boolean;
|
doingGeoloc?: boolean;
|
||||||
|
distance: number | null;
|
||||||
}>();
|
}>();
|
||||||
defineEmits(["doGeoLoc"]);
|
defineEmits(["doGeoLoc"]);
|
||||||
|
|
||||||
|
@ -112,17 +113,17 @@ const geoHash = computed(() => {
|
||||||
return geo;
|
return geo;
|
||||||
});
|
});
|
||||||
|
|
||||||
const distance = computed<number>(() =>
|
const distance = computed<number>(() => {
|
||||||
userLocation.value?.isIPLocation ? 150 : 25
|
return props.distance | 25;
|
||||||
);
|
});
|
||||||
|
|
||||||
const eventsQuery = useQuery<{
|
const eventsQuery = useQuery<{
|
||||||
searchEvents: Paginate<IEvent>;
|
searchEvents: Paginate<IEvent>;
|
||||||
}>(FETCH_EVENTS, () => ({
|
}>(FETCH_EVENTS, () => ({
|
||||||
orderBy: EventSortField.BEGINS_ON,
|
orderBy: EventSortField.BEGINS_ON,
|
||||||
direction: SortDirection.ASC,
|
direction: SortDirection.ASC,
|
||||||
longevents: false,
|
longEvents: false,
|
||||||
location: geoHash.value,
|
location: geoHash.value ?? "",
|
||||||
radius: distance.value,
|
radius: distance.value,
|
||||||
limit: 93,
|
limit: 93,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
<template>
|
|
||||||
<close-content
|
|
||||||
class="container mx-auto px-2"
|
|
||||||
v-show="loading || selectedGroups.length > 0"
|
|
||||||
@do-geo-loc="emit('doGeoLoc')"
|
|
||||||
:suggestGeoloc="userLocation.isIPLocation"
|
|
||||||
:doingGeoloc="doingGeoloc"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<template v-if="userLocationName">
|
|
||||||
{{
|
|
||||||
t("Popular groups nearby {position}", {
|
|
||||||
position: userLocationName,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ t("Popular groups close to you") }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<skeleton-group-result
|
|
||||||
v-for="i in [...Array(6).keys()]"
|
|
||||||
class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4"
|
|
||||||
:key="i"
|
|
||||||
v-show="loading"
|
|
||||||
/>
|
|
||||||
<group-card
|
|
||||||
v-for="group in selectedGroups"
|
|
||||||
:key="group.id"
|
|
||||||
:group="group"
|
|
||||||
:mode="'column'"
|
|
||||||
:showSummary="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<more-content
|
|
||||||
v-if="userLocationName"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.SEARCH,
|
|
||||||
query: {
|
|
||||||
locationName: userLocationName,
|
|
||||||
lat: userLocation.lat?.toString(),
|
|
||||||
lon: userLocation.lon?.toString(),
|
|
||||||
contentType: 'GROUPS',
|
|
||||||
distance: `${distance}_km`,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
:picture="userLocation.picture"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
t("View more groups around {position}", {
|
|
||||||
position: userLocationName,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</more-content>
|
|
||||||
</template>
|
|
||||||
</close-content>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import SkeletonGroupResult from "@/components/Group/SkeletonGroupResult.vue";
|
|
||||||
import sampleSize from "lodash/sampleSize";
|
|
||||||
import { LocationType } from "@/types/user-location.model";
|
|
||||||
import MoreContent from "./MoreContent.vue";
|
|
||||||
import CloseContent from "./CloseContent.vue";
|
|
||||||
import { IGroup } from "@/types/actor";
|
|
||||||
import { SEARCH_GROUPS } from "@/graphql/search";
|
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
import { computed } from "vue";
|
|
||||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
|
||||||
import { coordsToGeoHash } from "@/utils/location";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import RouteName from "@/router/name";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
userLocation: LocationType;
|
|
||||||
doingGeoloc?: boolean;
|
|
||||||
}>();
|
|
||||||
const emit = defineEmits(["doGeoLoc"]);
|
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
|
||||||
|
|
||||||
const userLocation = computed(() => props.userLocation);
|
|
||||||
|
|
||||||
const geoHash = computed(() =>
|
|
||||||
coordsToGeoHash(userLocation.value.lat, userLocation.value.lon)
|
|
||||||
);
|
|
||||||
|
|
||||||
const distance = computed<number>(() =>
|
|
||||||
userLocation.value?.isIPLocation ? 150 : 25
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result: groupsResult, loading: loadingGroups } = useQuery<{
|
|
||||||
searchGroups: Paginate<IGroup>;
|
|
||||||
}>(
|
|
||||||
SEARCH_GROUPS,
|
|
||||||
() => ({
|
|
||||||
location: geoHash.value,
|
|
||||||
radius: distance.value,
|
|
||||||
page: 1,
|
|
||||||
limit: 12,
|
|
||||||
}),
|
|
||||||
() => ({ enabled: geoHash.value !== undefined })
|
|
||||||
);
|
|
||||||
|
|
||||||
const groups = computed(
|
|
||||||
() => groupsResult.value?.searchGroups ?? { total: 0, elements: [] }
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedGroups = computed(() => sampleSize(groups.value?.elements, 5));
|
|
||||||
|
|
||||||
const userLocationName = computed(() => props?.userLocation?.name);
|
|
||||||
|
|
||||||
const loading = computed(() => props.doingGeoloc || loadingGroups.value);
|
|
||||||
</script>
|
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
<close-content
|
|
||||||
class="container mx-auto px-2"
|
|
||||||
v-show="loadingEvents || (events && events.total > 0)"
|
|
||||||
:suggestGeoloc="false"
|
|
||||||
v-on="attrs"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
{{ t("Agenda") }}
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<skeleton-event-result
|
|
||||||
v-for="i in 6"
|
|
||||||
class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4"
|
|
||||||
:key="i"
|
|
||||||
v-show="loadingEvents"
|
|
||||||
/>
|
|
||||||
<event-card
|
|
||||||
v-for="event in events.elements"
|
|
||||||
:event="event"
|
|
||||||
:key="event.uuid"
|
|
||||||
/>
|
|
||||||
<more-content
|
|
||||||
:to="{
|
|
||||||
name: RouteName.SEARCH,
|
|
||||||
query: {
|
|
||||||
contentType: 'EVENTS',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ t("View more events") }}
|
|
||||||
</more-content>
|
|
||||||
</template>
|
|
||||||
</close-content>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import MoreContent from "./MoreContent.vue";
|
|
||||||
import CloseContent from "./CloseContent.vue";
|
|
||||||
import { computed, useAttrs } from "vue";
|
|
||||||
import { IEvent } from "@/types/event.model";
|
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
|
||||||
import EventCard from "../Event/EventCard.vue";
|
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
|
|
||||||
import { EventSortField, SortDirection } from "@/types/enums";
|
|
||||||
import { FETCH_EVENTS } from "@/graphql/event";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import RouteName from "@/router/name";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
instanceName: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
|
||||||
const attrs = useAttrs();
|
|
||||||
|
|
||||||
const { result: resultEvents, loading: loadingEvents } = useQuery<{
|
|
||||||
events: Paginate<IEvent>;
|
|
||||||
}>(FETCH_EVENTS, {
|
|
||||||
orderBy: EventSortField.BEGINS_ON,
|
|
||||||
direction: SortDirection.ASC,
|
|
||||||
longevents: false,
|
|
||||||
});
|
|
||||||
const events = computed(
|
|
||||||
() => resultEvents.value?.events ?? { total: 0, elements: [] }
|
|
||||||
);
|
|
||||||
</script>
|
|
|
@ -1,75 +0,0 @@
|
||||||
<template>
|
|
||||||
<close-content
|
|
||||||
class="container mx-auto px-2"
|
|
||||||
:suggest-geoloc="false"
|
|
||||||
v-show="loadingEvents || (events?.elements && events?.elements.length > 0)"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
{{ $t("Online upcoming events") }}
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<skeleton-event-result
|
|
||||||
v-for="i in [...Array(6).keys()]"
|
|
||||||
class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4"
|
|
||||||
:key="i"
|
|
||||||
v-show="loadingEvents"
|
|
||||||
/>
|
|
||||||
<event-card
|
|
||||||
class="scroll-ml-6 snap-center shrink-0 first:pl-8 last:pr-8 w-[18rem]"
|
|
||||||
v-for="event in events?.elements"
|
|
||||||
:key="event.id"
|
|
||||||
:event="event"
|
|
||||||
mode="column"
|
|
||||||
/>
|
|
||||||
<more-content
|
|
||||||
:to="{
|
|
||||||
name: RouteName.SEARCH,
|
|
||||||
query: {
|
|
||||||
contentType: 'EVENTS',
|
|
||||||
isOnline: 'true',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
:picture="{
|
|
||||||
url: '/img/online-event.webp',
|
|
||||||
author: {
|
|
||||||
name: 'Chris Montgomery',
|
|
||||||
url: 'https://unsplash.com/@cwmonty',
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
name: 'Unsplash',
|
|
||||||
url: 'https://unsplash.com/?utm_source=Mobilizon&utm_medium=referral',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ $t("View more online events") }}
|
|
||||||
</more-content>
|
|
||||||
</template>
|
|
||||||
</close-content>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from "vue";
|
|
||||||
import SkeletonEventResult from "@/components/Event/SkeletonEventResult.vue";
|
|
||||||
import MoreContent from "./MoreContent.vue";
|
|
||||||
import CloseContent from "./CloseContent.vue";
|
|
||||||
import { SEARCH_EVENTS } from "@/graphql/search";
|
|
||||||
import EventCard from "@/components/Event/EventCard.vue";
|
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
|
||||||
import RouteName from "@/router/name";
|
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
import { IEvent } from "@/types/event.model";
|
|
||||||
|
|
||||||
const EVENT_PAGE_LIMIT = 12;
|
|
||||||
|
|
||||||
const { result: searchEventResult, loading: loadingEvents } = useQuery<{
|
|
||||||
searchEvents: Paginate<IEvent>;
|
|
||||||
}>(SEARCH_EVENTS, () => ({
|
|
||||||
beginsOn: new Date(),
|
|
||||||
endsOn: undefined,
|
|
||||||
eventPage: 1,
|
|
||||||
limit: EVENT_PAGE_LIMIT,
|
|
||||||
type: "ONLINE",
|
|
||||||
}));
|
|
||||||
|
|
||||||
const events = computed(() => searchEventResult.value?.searchEvents);
|
|
||||||
</script>
|
|
|
@ -11,8 +11,24 @@
|
||||||
>
|
>
|
||||||
<MobilizonLogo class="w-40" />
|
<MobilizonLogo class="w-40" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<div
|
||||||
<div class="flex items-center md:order-2 ml-auto" v-if="currentActor?.id">
|
class="flex items-center md:order-2 ml-auto gap-2"
|
||||||
|
v-if="currentActor?.id"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: RouteName.CONVERSATION_LIST }"
|
||||||
|
class="flex sm:mr-3 text-sm md:mr-0 relative"
|
||||||
|
id="conversations-menu-button"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ t("Open conversations") }}</span>
|
||||||
|
<Inbox :size="32" />
|
||||||
|
<span
|
||||||
|
v-show="unreadConversationsCount > 0"
|
||||||
|
class="absolute bottom-0.5 -left-2 bg-primary rounded-full inline-block h-3 w-3 mx-2"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
<o-dropdown position="bottom-right">
|
<o-dropdown position="bottom-right">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
|
@ -163,47 +179,6 @@
|
||||||
<ul
|
<ul
|
||||||
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
|
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
|
||||||
>
|
>
|
||||||
<search-fields
|
|
||||||
v-if="showMobileMenu"
|
|
||||||
class="m-auto w-auto"
|
|
||||||
v-model:search="search"
|
|
||||||
v-model:location="location"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<li class="m-auto" v-if="islongEvents">
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: RouteName.SEARCH,
|
|
||||||
query: { contentType: 'SHORTEVENTS' },
|
|
||||||
}"
|
|
||||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
|
||||||
>{{ t("Events") }}</router-link
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="m-auto" v-else>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: RouteName.SEARCH, query: { contentType: 'EVENTS' } }"
|
|
||||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
|
||||||
>{{ t("Events") }}</router-link
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="m-auto" v-if="islongEvents">
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: RouteName.SEARCH,
|
|
||||||
query: { contentType: 'LONGEVENTS' },
|
|
||||||
}"
|
|
||||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
|
||||||
>{{ t("Activities") }}</router-link
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="m-auto">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: RouteName.SEARCH, query: { contentType: 'GROUPS' } }"
|
|
||||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
|
||||||
>{{ t("Groups") }}</router-link
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="m-auto">
|
<li class="m-auto">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.EVENT_CALENDAR }"
|
:to="{ name: RouteName.EVENT_CALENDAR }"
|
||||||
|
@ -242,13 +217,6 @@
|
||||||
>{{ t("Register") }}</router-link
|
>{{ t("Register") }}</router-link
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<search-fields
|
|
||||||
v-if="!showMobileMenu"
|
|
||||||
class="m-auto w-auto"
|
|
||||||
v-model:search="search"
|
|
||||||
v-model:location="location"
|
|
||||||
/>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -273,15 +241,10 @@ import {
|
||||||
import { useMutation } from "@vue/apollo-composable";
|
import { useMutation } from "@vue/apollo-composable";
|
||||||
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
||||||
import { changeIdentity } from "@/utils/identity";
|
import { changeIdentity } from "@/utils/identity";
|
||||||
import {
|
import { useRegistrationConfig } from "@/composition/apollo/config";
|
||||||
useRegistrationConfig,
|
|
||||||
useIsLongEvents,
|
|
||||||
} from "@/composition/apollo/config";
|
|
||||||
import { useOruga } from "@oruga-ui/oruga-next";
|
import { useOruga } from "@oruga-ui/oruga-next";
|
||||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||||
|
|
||||||
const { islongEvents } = useIsLongEvents();
|
|
||||||
|
|
||||||
const { currentUser } = useCurrentUserClient();
|
const { currentUser } = useCurrentUserClient();
|
||||||
const { currentActor } = useCurrentActorClient();
|
const { currentActor } = useCurrentActorClient();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="posts-wrapper grid gap-4">
|
<div class="posts-wrapper grid gap-2">
|
||||||
<post-list-item
|
<post-list-item
|
||||||
v-for="post in posts"
|
v-for="post in posts"
|
||||||
:key="post.id"
|
:key="post.id"
|
||||||
|
|
|
@ -41,12 +41,12 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// import { SnackbarProgrammatic as Snackbar } from "buefy";
|
// import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||||
import { doUpdateSetting, useUserSettings } from "@/composition/apollo/user";
|
import { doUpdateSetting, useLoggedUser } from "@/composition/apollo/user";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
const notificationOnDay = ref(true);
|
const notificationOnDay = ref(true);
|
||||||
|
|
||||||
const { loggedUser } = useUserSettings();
|
const { loggedUser } = useLoggedUser();
|
||||||
|
|
||||||
const updateSetting = async (
|
const updateSetting = async (
|
||||||
variables: Record<string, unknown>
|
variables: Record<string, unknown>
|
||||||
|
|
|
@ -55,7 +55,7 @@ import { useTimezones } from "@/composition/apollo/config";
|
||||||
import {
|
import {
|
||||||
doUpdateSetting,
|
doUpdateSetting,
|
||||||
updateLocale,
|
updateLocale,
|
||||||
useUserSettings,
|
useLoggedUser,
|
||||||
} from "@/composition/apollo/user";
|
} from "@/composition/apollo/user";
|
||||||
import { saveLocaleData } from "@/utils/auth";
|
import { saveLocaleData } from "@/utils/auth";
|
||||||
import { computed, onMounted, watch } from "vue";
|
import { computed, onMounted, watch } from "vue";
|
||||||
|
@ -68,7 +68,7 @@ const { t, locale } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
const { loggedUser } = useUserSettings();
|
const { loggedUser } = useLoggedUser();
|
||||||
|
|
||||||
const { mutate: doUpdateLocale } = updateLocale();
|
const { mutate: doUpdateLocale } = updateLocale();
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,10 @@ const icons: Record<string, () => Promise<any>> = {
|
||||||
import(`../../../node_modules/vue-material-design-icons/LinkOff.vue`),
|
import(`../../../node_modules/vue-material-design-icons/LinkOff.vue`),
|
||||||
Image: () =>
|
Image: () =>
|
||||||
import(`../../../node_modules/vue-material-design-icons/Image.vue`),
|
import(`../../../node_modules/vue-material-design-icons/Image.vue`),
|
||||||
|
Information: () =>
|
||||||
|
import(
|
||||||
|
`../../../node_modules/vue-material-design-icons/InformationVariant.vue`
|
||||||
|
),
|
||||||
FormatListBulleted: () =>
|
FormatListBulleted: () =>
|
||||||
import(
|
import(
|
||||||
`../../../node_modules/vue-material-design-icons/FormatListBulleted.vue`
|
`../../../node_modules/vue-material-design-icons/FormatListBulleted.vue`
|
||||||
|
@ -111,6 +115,10 @@ const icons: Record<string, () => Promise<any>> = {
|
||||||
Map: () => import(`../../../node_modules/vue-material-design-icons/Map.vue`),
|
Map: () => import(`../../../node_modules/vue-material-design-icons/Map.vue`),
|
||||||
MapMarker: () =>
|
MapMarker: () =>
|
||||||
import(`../../../node_modules/vue-material-design-icons/MapMarker.vue`),
|
import(`../../../node_modules/vue-material-design-icons/MapMarker.vue`),
|
||||||
|
MapMarkerDistance: () =>
|
||||||
|
import(
|
||||||
|
`../../../node_modules/vue-material-design-icons/MapMarkerDistance.vue`
|
||||||
|
),
|
||||||
Close: () =>
|
Close: () =>
|
||||||
import(`../../../node_modules/vue-material-design-icons/Close.vue`),
|
import(`../../../node_modules/vue-material-design-icons/Close.vue`),
|
||||||
Magnify: () =>
|
Magnify: () =>
|
||||||
|
|
|
@ -227,8 +227,8 @@ export function useIsLongEvents() {
|
||||||
config: Pick<IConfig, "longEvents">;
|
config: Pick<IConfig, "longEvents">;
|
||||||
}>(LONG_EVENTS);
|
}>(LONG_EVENTS);
|
||||||
|
|
||||||
const islongEvents = computed(() => result.value?.config.longEvents);
|
const isLongEvents = computed(() => result.value?.config.longEvents);
|
||||||
return { islongEvents, error, loading };
|
return { isLongEvents, error, loading };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAnalytics() {
|
export function useAnalytics() {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { IDENTITIES, REGISTER_PERSON } from "@/graphql/actor";
|
import { IDENTITIES, REGISTER_PERSON } from "@/graphql/actor";
|
||||||
import {
|
import {
|
||||||
CURRENT_USER_CLIENT,
|
CURRENT_USER_CLIENT,
|
||||||
LOGGED_USER,
|
LOGGED_USER_AND_SETTINGS,
|
||||||
LOGGED_USER_LOCATION,
|
LOGGED_USER_LOCATION,
|
||||||
SET_USER_SETTINGS,
|
SET_USER_SETTINGS,
|
||||||
UPDATE_USER_LOCALE,
|
UPDATE_USER_LOCALE,
|
||||||
USER_SETTINGS,
|
|
||||||
} from "@/graphql/user";
|
} from "@/graphql/user";
|
||||||
import { IPerson } from "@/types/actor";
|
import { IPerson } from "@/types/actor";
|
||||||
import { ICurrentUser, IUser } from "@/types/current-user.model";
|
import { ICurrentUser, IUser } from "@/types/current-user.model";
|
||||||
|
@ -31,25 +30,14 @@ export function useCurrentUserClient() {
|
||||||
export function useLoggedUser() {
|
export function useLoggedUser() {
|
||||||
const { currentUser } = useCurrentUserClient();
|
const { currentUser } = useCurrentUserClient();
|
||||||
|
|
||||||
const { result, error, onError } = useQuery<{ loggedUser: IUser }>(
|
const { result, error, onError, loading } = useQuery<{ loggedUser: IUser }>(
|
||||||
LOGGED_USER,
|
LOGGED_USER_AND_SETTINGS,
|
||||||
{},
|
{},
|
||||||
() => ({ enabled: currentUser.value?.id != null })
|
() => ({ enabled: currentUser.value?.id != null })
|
||||||
);
|
);
|
||||||
|
|
||||||
const loggedUser = computed(() => result.value?.loggedUser);
|
const loggedUser = computed(() => result.value?.loggedUser);
|
||||||
return { loggedUser, error, onError };
|
return { loggedUser, error, onError, loading };
|
||||||
}
|
|
||||||
|
|
||||||
export function useUserSettings() {
|
|
||||||
const {
|
|
||||||
result: userSettingsResult,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
} = useQuery<{ loggedUser: IUser }>(USER_SETTINGS);
|
|
||||||
|
|
||||||
const loggedUser = computed(() => userSettingsResult.value?.loggedUser);
|
|
||||||
return { loggedUser, error, loading };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserLocation() {
|
export function useUserLocation() {
|
||||||
|
|
|
@ -8,12 +8,13 @@ function formatDateISOStringWithoutTime(value: string): string {
|
||||||
return parseDateTime(value).toISOString().split("T")[0];
|
return parseDateTime(value).toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateString(value: string): string {
|
function formatDateString(value: string, timeZone?: string): string {
|
||||||
return parseDateTime(value).toLocaleString(locale(), {
|
return parseDateTime(value).toLocaleString(locale(), {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
timeZone: timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ function formatTimeString(value: string, timeZone?: string): string {
|
||||||
return parseDateTime(value).toLocaleTimeString(locale(), {
|
return parseDateTime(value).toLocaleTimeString(locale(), {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
timeZone,
|
timeZone: timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ export const FETCH_EVENTS = gql`
|
||||||
$direction: SortDirection
|
$direction: SortDirection
|
||||||
$page: Int
|
$page: Int
|
||||||
$limit: Int
|
$limit: Int
|
||||||
$longevents: Boolean
|
$longEvents: Boolean
|
||||||
) {
|
) {
|
||||||
events(
|
events(
|
||||||
location: $location
|
location: $location
|
||||||
|
@ -118,7 +118,7 @@ export const FETCH_EVENTS = gql`
|
||||||
direction: $direction
|
direction: $direction
|
||||||
page: $page
|
page: $page
|
||||||
limit: $limit
|
limit: $limit
|
||||||
longevents: $longevents
|
longEvents: $longEvents
|
||||||
) {
|
) {
|
||||||
total
|
total
|
||||||
elements {
|
elements {
|
||||||
|
|
|
@ -150,6 +150,7 @@ export const GROUP_BASIC_FIELDS_FRAGMENTS = gql`
|
||||||
beginsOn
|
beginsOn
|
||||||
status
|
status
|
||||||
draft
|
draft
|
||||||
|
longEvent
|
||||||
language
|
language
|
||||||
options {
|
options {
|
||||||
maximumAttendeeCapacity
|
maximumAttendeeCapacity
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
||||||
$searchTarget: SearchTarget
|
$searchTarget: SearchTarget
|
||||||
$beginsOn: DateTime
|
$beginsOn: DateTime
|
||||||
$endsOn: DateTime
|
$endsOn: DateTime
|
||||||
$longevents: Boolean
|
$longEvents: Boolean
|
||||||
$bbox: String
|
$bbox: String
|
||||||
$zoom: Int
|
$zoom: Int
|
||||||
$eventPage: Int
|
$eventPage: Int
|
||||||
|
@ -55,7 +55,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
||||||
searchTarget: $searchTarget
|
searchTarget: $searchTarget
|
||||||
beginsOn: $beginsOn
|
beginsOn: $beginsOn
|
||||||
endsOn: $endsOn
|
endsOn: $endsOn
|
||||||
longevents: $longevents
|
longEvents: $longEvents
|
||||||
bbox: $bbox
|
bbox: $bbox
|
||||||
zoom: $zoom
|
zoom: $zoom
|
||||||
page: $eventPage
|
page: $eventPage
|
||||||
|
@ -70,6 +70,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
||||||
uuid
|
uuid
|
||||||
beginsOn
|
beginsOn
|
||||||
endsOn
|
endsOn
|
||||||
|
longEvent
|
||||||
picture {
|
picture {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
|
@ -155,7 +156,7 @@ export const SEARCH_EVENTS = gql`
|
||||||
$endsOn: DateTime
|
$endsOn: DateTime
|
||||||
$eventPage: Int
|
$eventPage: Int
|
||||||
$limit: Int
|
$limit: Int
|
||||||
$longevents: Boolean
|
$longEvents: Boolean
|
||||||
) {
|
) {
|
||||||
searchEvents(
|
searchEvents(
|
||||||
location: $location
|
location: $location
|
||||||
|
@ -168,7 +169,7 @@ export const SEARCH_EVENTS = gql`
|
||||||
endsOn: $endsOn
|
endsOn: $endsOn
|
||||||
page: $eventPage
|
page: $eventPage
|
||||||
limit: $limit
|
limit: $limit
|
||||||
longevents: $longevents
|
longEvents: $longEvents
|
||||||
) {
|
) {
|
||||||
total
|
total
|
||||||
elements {
|
elements {
|
||||||
|
@ -218,7 +219,7 @@ export const SEARCH_CALENDAR_EVENTS = gql`
|
||||||
endsOn: $endsOn
|
endsOn: $endsOn
|
||||||
page: $eventPage
|
page: $eventPage
|
||||||
limit: $limit
|
limit: $limit
|
||||||
longevents: false
|
longEvents: false
|
||||||
) {
|
) {
|
||||||
total
|
total
|
||||||
elements {
|
elements {
|
||||||
|
|
|
@ -125,6 +125,25 @@ export const USER_SETTINGS_FRAGMENT = gql`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LOGGED_USER_AND_SETTINGS = gql`
|
||||||
|
query LoggedUserQuery {
|
||||||
|
loggedUser {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
locale
|
||||||
|
provider
|
||||||
|
defaultActor {
|
||||||
|
...ActorFragment
|
||||||
|
}
|
||||||
|
settings {
|
||||||
|
...UserSettingFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${ACTOR_FRAGMENT}
|
||||||
|
${USER_SETTINGS_FRAGMENT}
|
||||||
|
`;
|
||||||
|
|
||||||
export const USER_SETTINGS = gql`
|
export const USER_SETTINGS = gql`
|
||||||
query UserSetting {
|
query UserSetting {
|
||||||
loggedUser {
|
loggedUser {
|
||||||
|
|
|
@ -414,7 +414,7 @@
|
||||||
"Due on": "Due on",
|
"Due on": "Due on",
|
||||||
"Organizers": "Organizers",
|
"Organizers": "Organizers",
|
||||||
"(Masked)": "(Masked)",
|
"(Masked)": "(Masked)",
|
||||||
"{available}/{capacity} available places": "No places left|{available}/{capacity} available places",
|
"{available}/{capacity} available places": "No places left|{available}/{capacity} available place left|{available}/{capacity} available places",
|
||||||
"No one is participating|One person participating|{going} people participating": "No one is participating|One person participating|{going} people participating",
|
"No one is participating|One person participating|{going} people participating": "No one is participating|One person participating|{going} people participating",
|
||||||
"Date and time": "Date and time",
|
"Date and time": "Date and time",
|
||||||
"Location": "Location",
|
"Location": "Location",
|
||||||
|
@ -1218,7 +1218,7 @@
|
||||||
"You will receive notifications about this group's public activity depending on %{notification_settings}.": "You will receive notifications about this group's public activity depending on %{notification_settings}.",
|
"You will receive notifications about this group's public activity depending on %{notification_settings}.": "You will receive notifications about this group's public activity depending on %{notification_settings}.",
|
||||||
"Online": "Online",
|
"Online": "Online",
|
||||||
"That you follow or of which you are a member": "That you follow or of which you are a member",
|
"That you follow or of which you are a member": "That you follow or of which you are a member",
|
||||||
"{number} seats left": "{number} seats left",
|
"{number} seats left": "No seat left|One seat left|{number} seats left",
|
||||||
"Published by {name}": "Published by {name}",
|
"Published by {name}": "Published by {name}",
|
||||||
"Share this post": "Share this post",
|
"Share this post": "Share this post",
|
||||||
"This post is accessible only through it's link. Be careful where you post this link.": "This post is accessible only through it's link. Be careful where you post this link.",
|
"This post is accessible only through it's link. Be careful where you post this link.": "This post is accessible only through it's link. Be careful where you post this link.",
|
||||||
|
@ -1360,6 +1360,7 @@
|
||||||
"Keyword, event title, group name, etc.": "Keyword, event title, group name, etc.",
|
"Keyword, event title, group name, etc.": "Keyword, event title, group name, etc.",
|
||||||
"Go!": "Go!",
|
"Go!": "Go!",
|
||||||
"Explore!": "Explore!",
|
"Explore!": "Explore!",
|
||||||
|
"Select distance": "Select distance",
|
||||||
"Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance",
|
"Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance",
|
||||||
"Open user menu": "Open user menu",
|
"Open user menu": "Open user menu",
|
||||||
"Open main menu": "Open main menu",
|
"Open main menu": "Open main menu",
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
"+ Add a resource": "+ Ajouter une ressource",
|
"+ Add a resource": "+ Ajouter une ressource",
|
||||||
"+ Create a post": "+ Créer un billet",
|
"+ Create a post": "+ Créer un billet",
|
||||||
"+ Create an event": "+ Créer un événement",
|
"+ Create an event": "+ Créer un événement",
|
||||||
|
"+ Create an activity": "+ Créer une activité",
|
||||||
"+ Start a discussion": "+ Lancer une discussion",
|
"+ Start a discussion": "+ Lancer une discussion",
|
||||||
|
"+ View past activities": "+ Voir les activités passées",
|
||||||
|
"+ View past events": "+ Voir les événements passés",
|
||||||
"0 Bytes": "0 octets",
|
"0 Bytes": "0 octets",
|
||||||
"<b>{contact}</b> will be displayed as contact.": "<b>{contact}</b> sera affiché·e comme contact.|<b>{contact}</b> seront affiché·e·s comme contacts.",
|
"<b>{contact}</b> will be displayed as contact.": "<b>{contact}</b> sera affiché·e comme contact.|<b>{contact}</b> seront affiché·e·s comme contacts.",
|
||||||
"@{group}": "@{group}",
|
"@{group}": "@{group}",
|
||||||
|
@ -33,7 +36,7 @@
|
||||||
"A resource has been created or updated": "Une resource a été créée ou mise à jour",
|
"A resource has been created or updated": "Une resource a été créée ou mise à jour",
|
||||||
"A twitter account handle to follow for event updates": "Un compte sur Twitter à suivre pour les mises à jour de l'événement",
|
"A twitter account handle to follow for event updates": "Un compte sur Twitter à suivre pour les mises à jour de l'événement",
|
||||||
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
|
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
|
||||||
"A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
|
"A validation email was sent to {email}": "Un e-mail de validation a été envoyé à {email}",
|
||||||
"API": "API",
|
"API": "API",
|
||||||
"Abandon editing": "Abandonner la modification",
|
"Abandon editing": "Abandonner la modification",
|
||||||
"About": "À propos",
|
"About": "À propos",
|
||||||
|
@ -69,6 +72,7 @@
|
||||||
"Activate browser push notifications": "Activer les notifications push du navigateur",
|
"Activate browser push notifications": "Activer les notifications push du navigateur",
|
||||||
"Activate notifications": "Activer les notifications",
|
"Activate notifications": "Activer les notifications",
|
||||||
"Activated": "Activé",
|
"Activated": "Activé",
|
||||||
|
"Activities": "Activités",
|
||||||
"Active": "Actif·ive",
|
"Active": "Actif·ive",
|
||||||
"Activity": "Activité",
|
"Activity": "Activité",
|
||||||
"Actor": "Acteur",
|
"Actor": "Acteur",
|
||||||
|
@ -83,7 +87,7 @@
|
||||||
"Add an address": "Ajouter une adresse",
|
"Add an address": "Ajouter une adresse",
|
||||||
"Add an instance": "Ajouter une instance",
|
"Add an instance": "Ajouter une instance",
|
||||||
"Add link": "Ajouter un lien",
|
"Add link": "Ajouter un lien",
|
||||||
"Add new…": "Ajouter un nouvel élément…",
|
"Add new…": "Ajouter…",
|
||||||
"Add picture": "Ajouter une image",
|
"Add picture": "Ajouter une image",
|
||||||
"Add some tags": "Ajouter des tags",
|
"Add some tags": "Ajouter des tags",
|
||||||
"Add to my calendar": "Ajouter à mon agenda",
|
"Add to my calendar": "Ajouter à mon agenda",
|
||||||
|
@ -112,7 +116,7 @@
|
||||||
"An event I'm organizing has a new pending participation": "Un événement que j'organise a une nouvelle participation en attente",
|
"An event I'm organizing has a new pending participation": "Un événement que j'organise a une nouvelle participation en attente",
|
||||||
"An event from one of my groups has been published": "Un événement d'un de mes groupes a été publié",
|
"An event from one of my groups has been published": "Un événement d'un de mes groupes a été publié",
|
||||||
"An event from one of my groups has been updated or deleted": "Un événement d'un de mes groupes a été mis à jour ou supprimé",
|
"An event from one of my groups has been updated or deleted": "Un événement d'un de mes groupes a été mis à jour ou supprimé",
|
||||||
"An instance is an installed version of the Mobilizon software running on a server. An instance can be run by anyone using the {mobilizon_software} or other federated apps, aka the “fediverse”. This instance's name is {instance_name}. Mobilizon is a federated network of multiple instances (just like email servers), users registered on different instances may communicate even though they didn't register on the same instance.": "Une instance est une version du logiciel Mobilizon fonctionnant sur un serveur. Une instance peut être gérée par n'importe qui avec le {mobilizon_software} ou d'autres applications fédérées, correspondant au « fediverse ». Cette instance se nomme {instance_name}. Mobilizon est un réseau fédéré de multiples instances (tout comme des serveurs email), des utilisateur·rice·s inscrites sur différentes instances peuvent communiquer bien qu'il·elle·s ne se soient pas enregistré·e·s sur la même instance.",
|
"An instance is an installed version of the Mobilizon software running on a server. An instance can be run by anyone using the {mobilizon_software} or other federated apps, aka the “fediverse”. This instance's name is {instance_name}. Mobilizon is a federated network of multiple instances (just like email servers), users registered on different instances may communicate even though they didn't register on the same instance.": "Une instance est une version du logiciel Mobilizon fonctionnant sur un serveur. Une instance peut être gérée par n'importe qui avec le {mobilizon_software} ou d'autres applications fédérées, correspondant au « fediverse ». Cette instance se nomme {instance_name}. Mobilizon est un réseau fédéré de multiples instances (tout comme des serveurs e-mail), des utilisateur·rice·s inscrites sur différentes instances peuvent communiquer bien qu'il·elle·s ne se soient pas enregistré·e·s sur la même instance.",
|
||||||
"An “application programming interface” or “API” is a communication protocol that allows software components to communicate with each other. The Mobilizon API, for example, can allow third-party software tools to communicate with Mobilizon instances to carry out certain actions, such as posting events on your behalf, automatically and remotely.": "Une « interface de programmation d’application » ou « API » est un protocole de communication qui permet aux composants logiciels de communiquer entre eux. L'API Mobilizon, par exemple, peut permettre à des outils logiciels tiers de communiquer avec les instances Mobilizon pour effectuer certaines actions, telles que la publication d'événements en votre nom, automatiquement et à distance.",
|
"An “application programming interface” or “API” is a communication protocol that allows software components to communicate with each other. The Mobilizon API, for example, can allow third-party software tools to communicate with Mobilizon instances to carry out certain actions, such as posting events on your behalf, automatically and remotely.": "Une « interface de programmation d’application » ou « API » est un protocole de communication qui permet aux composants logiciels de communiquer entre eux. L'API Mobilizon, par exemple, peut permettre à des outils logiciels tiers de communiquer avec les instances Mobilizon pour effectuer certaines actions, telles que la publication d'événements en votre nom, automatiquement et à distance.",
|
||||||
"An “application programming interface” or “API” is a communication protocol that allows software components to communicate with each other. The Mobilizon API, for example, can allow third-party software tools to communicate with Mobilizon instances to carry out certain actions, such as posting events, automatically and remotely.": "Une « interface de programmation d'application » ou « API » est un protocole de communication qui permet à des composants logiciels de communiquer entre eux. L'API de Mobilizon, par exemple, peut permettre à des outils logiciels tiers de communiquer avec des instances de Mobilizon pour effectuer certaines actions, comme la publication d'événements, automatiquement et à distance.",
|
"An “application programming interface” or “API” is a communication protocol that allows software components to communicate with each other. The Mobilizon API, for example, can allow third-party software tools to communicate with Mobilizon instances to carry out certain actions, such as posting events, automatically and remotely.": "Une « interface de programmation d'application » ou « API » est un protocole de communication qui permet à des composants logiciels de communiquer entre eux. L'API de Mobilizon, par exemple, peut permettre à des outils logiciels tiers de communiquer avec des instances de Mobilizon pour effectuer certaines actions, comme la publication d'événements, automatiquement et à distance.",
|
||||||
"And {number} comments": "Et {number} commentaires",
|
"And {number} comments": "Et {number} commentaires",
|
||||||
|
@ -120,7 +124,7 @@
|
||||||
"Announcements and mentions notifications are always sent straight away.": "Les notifications d'annonces et de mentions sont toujours envoyées directement.",
|
"Announcements and mentions notifications are always sent straight away.": "Les notifications d'annonces et de mentions sont toujours envoyées directement.",
|
||||||
"Announcements for {eventTitle}": "Annonces pour {eventTitle}",
|
"Announcements for {eventTitle}": "Annonces pour {eventTitle}",
|
||||||
"Anonymous participant": "Participant·e anonyme",
|
"Anonymous participant": "Participant·e anonyme",
|
||||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par email.",
|
"Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par e-mail.",
|
||||||
"Anonymous participations": "Participations anonymes",
|
"Anonymous participations": "Participations anonymes",
|
||||||
"Any category": "N'importe quelle catégorie",
|
"Any category": "N'importe quelle catégorie",
|
||||||
"Any day": "N'importe quand",
|
"Any day": "N'importe quand",
|
||||||
|
@ -187,7 +191,7 @@
|
||||||
"By {group}": "Par {group}",
|
"By {group}": "Par {group}",
|
||||||
"By {username}": "Par {username}",
|
"By {username}": "Par {username}",
|
||||||
"Calendar": "Calendrier",
|
"Calendar": "Calendrier",
|
||||||
"Can be an email or a link, or just plain text.": "Peut être une adresse email ou bien un lien, ou alors du simple texte brut.",
|
"Can be an email or a link, or just plain text.": "Peut être une adresse e-mail ou bien un lien, ou alors du simple texte brut.",
|
||||||
"Cancel": "Annuler",
|
"Cancel": "Annuler",
|
||||||
"Cancel anonymous participation": "Annuler ma participation anonyme",
|
"Cancel anonymous participation": "Annuler ma participation anonyme",
|
||||||
"Cancel creation": "Annuler la création",
|
"Cancel creation": "Annuler la création",
|
||||||
|
@ -205,14 +209,14 @@
|
||||||
"Category illustrations credits": "Crédits des illustrations des catégories",
|
"Category illustrations credits": "Crédits des illustrations des catégories",
|
||||||
"Category list": "Liste des catégories",
|
"Category list": "Liste des catégories",
|
||||||
"Change": "Modifier",
|
"Change": "Modifier",
|
||||||
"Change email": "Changer l'email",
|
"Change email": "Changer l'e-mail",
|
||||||
"Change my email": "Changer mon adresse e-mail",
|
"Change my email": "Changer mon adresse e-mail",
|
||||||
"Change my identity…": "Changer mon identité…",
|
"Change my identity…": "Changer mon identité…",
|
||||||
"Change my password": "Modifier mon mot de passe",
|
"Change my password": "Modifier mon mot de passe",
|
||||||
"Change role": "Changer le role",
|
"Change role": "Changer le role",
|
||||||
"Change the filters.": "Changez les filtres.",
|
"Change the filters.": "Changez les filtres.",
|
||||||
"Change timezone": "Changer de fuseau horaire",
|
"Change timezone": "Changer de fuseau horaire",
|
||||||
"Change user email": "Modifier l'email de l'utilisateur·ice",
|
"Change user email": "Modifier l'e-mail de l'utilisateur·ice",
|
||||||
"Change user role": "Changer le role de l'utilisateur",
|
"Change user role": "Changer le role de l'utilisateur",
|
||||||
"Check your device to continue. You may now close this window.": "Vérifiez votre appareil pour continuer. Vous pouvez maintenant fermer cette fenêtre.",
|
"Check your device to continue. You may now close this window.": "Vérifiez votre appareil pour continuer. Vous pouvez maintenant fermer cette fenêtre.",
|
||||||
"Check your inbox (and your junk mail folder).": "Vérifiez votre boîte de réception (et votre dossier des indésirables).",
|
"Check your inbox (and your junk mail folder).": "Vérifiez votre boîte de réception (et votre dossier des indésirables).",
|
||||||
|
@ -305,6 +309,8 @@
|
||||||
"Deactivate notifications": "Désactiver les notifications",
|
"Deactivate notifications": "Désactiver les notifications",
|
||||||
"Decline": "Refuser",
|
"Decline": "Refuser",
|
||||||
"Decrease": "Baisser",
|
"Decrease": "Baisser",
|
||||||
|
"Decreasing creation date": "Nouveaux groupes",
|
||||||
|
"Decreasing number of members": "Le plus de membres",
|
||||||
"Default": "Défaut",
|
"Default": "Défaut",
|
||||||
"Default Mobilizon privacy policy": "Politique de confidentialité par défaut de Mobilizon",
|
"Default Mobilizon privacy policy": "Politique de confidentialité par défaut de Mobilizon",
|
||||||
"Default Mobilizon terms": "Conditions d'utilisation par défaut de Mobilizon",
|
"Default Mobilizon terms": "Conditions d'utilisation par défaut de Mobilizon",
|
||||||
|
@ -369,21 +375,21 @@
|
||||||
"Edit": "Modifier",
|
"Edit": "Modifier",
|
||||||
"Edit post": "Éditer le billet",
|
"Edit post": "Éditer le billet",
|
||||||
"Edit profile {profile}": "Éditer le profil {profile}",
|
"Edit profile {profile}": "Éditer le profil {profile}",
|
||||||
"Edit user email": "Éditer l'email de l'utilisateur·ice",
|
"Edit user email": "Éditer l'e-mail de l'utilisateur·ice",
|
||||||
"Edited {ago}": "Édité il y a {ago}",
|
"Edited {ago}": "Édité il y a {ago}",
|
||||||
"Edited {relative_time} ago": "Édité il y a {relative_time}",
|
"Edited {relative_time} ago": "Édité il y a {relative_time}",
|
||||||
"Eg: Stockholm, Dance, Chess…": "Par exemple : Lyon, Danse, Bridge…",
|
"Eg: Stockholm, Dance, Chess…": "Par exemple : Lyon, Danse, Bridge…",
|
||||||
"Either on the {instance} instance or on another instance.": "Sur l'instance {instance} ou bien sur une autre instance.",
|
"Either on the {instance} instance or on another instance.": "Sur l'instance {instance} ou bien sur une autre instance.",
|
||||||
"Either the account is already validated, either the validation token is incorrect.": "Soit le compte est déjà validé, soit le jeton de validation est incorrect.",
|
"Either the account is already validated, either the validation token is incorrect.": "Soit le compte est déjà validé, soit le jeton de validation est incorrect.",
|
||||||
"Either the email has already been changed, either the validation token is incorrect.": "Soit l'adresse email a déjà été modifiée, soit le jeton de validation est incorrect.",
|
"Either the email has already been changed, either the validation token is incorrect.": "Soit l'adresse e-mail a déjà été modifiée, soit le jeton de validation est incorrect.",
|
||||||
"Either the participation request has already been validated, either the validation token is incorrect.": "Soit la demande de participation a déjà été validée, soit le jeton de validation est incorrect.",
|
"Either the participation request has already been validated, either the validation token is incorrect.": "Soit la demande de participation a déjà été validée, soit le jeton de validation est incorrect.",
|
||||||
"Either your participation has already been cancelled, either the validation token is incorrect.": "Soit votre participation a déjà été annulée, soit le jeton de validation est incorrect.",
|
"Either your participation has already been cancelled, either the validation token is incorrect.": "Soit votre participation a déjà été annulée, soit le jeton de validation est incorrect.",
|
||||||
"Element title": "Titre de l'élement",
|
"Element title": "Titre de l'élement",
|
||||||
"Element value": "Valeur de l'élement",
|
"Element value": "Valeur de l'élement",
|
||||||
"Email": "Courriel",
|
"Email": "Courriel",
|
||||||
"Email address": "Adresse email",
|
"Email address": "Adresse e-mail",
|
||||||
"Email validate": "Validation de l'email",
|
"Email validate": "Validation de l'e-mail",
|
||||||
"Emails usually don't contain capitals, make sure you haven't made a typo.": "Les emails ne contiennent d'ordinaire pas de capitales, assurez-vous de n'avoir pas fait de faute de frappe.",
|
"Emails usually don't contain capitals, make sure you haven't made a typo.": "Les e-mails ne contiennent d'ordinaire pas de capitales, assurez-vous de n'avoir pas fait de faute de frappe.",
|
||||||
"Enabled": "Activé",
|
"Enabled": "Activé",
|
||||||
"Ends on…": "Se termine le…",
|
"Ends on…": "Se termine le…",
|
||||||
"Enter the code displayed on your device": "Saisissez le code affiché sur votre appareil",
|
"Enter the code displayed on your device": "Saisissez le code affiché sur votre appareil",
|
||||||
|
@ -397,7 +403,7 @@
|
||||||
"Error stacktrace": "Trace d'appels de l'erreur",
|
"Error stacktrace": "Trace d'appels de l'erreur",
|
||||||
"Error while adding tag: {error}": "Erreur lors de l'ajout d'un tag : {error}",
|
"Error while adding tag: {error}": "Erreur lors de l'ajout d'un tag : {error}",
|
||||||
"Error while cancelling your participation": "Erreur lors de l'annulation de votre participation",
|
"Error while cancelling your participation": "Erreur lors de l'annulation de votre participation",
|
||||||
"Error while changing email": "Erreur lors de la modification de l'adresse email",
|
"Error while changing email": "Erreur lors de la modification de l'adresse e-mail",
|
||||||
"Error while loading the preview": "Erreur lors du chargement de l'aperçu",
|
"Error while loading the preview": "Erreur lors du chargement de l'aperçu",
|
||||||
"Error while login with {provider}. Retry or login another way.": "Erreur lors de la connexion avec {provider}. Réessayez ou bien connectez vous autrement.",
|
"Error while login with {provider}. Retry or login another way.": "Erreur lors de la connexion avec {provider}. Réessayez ou bien connectez vous autrement.",
|
||||||
"Error while login with {provider}. This login provider doesn't exist.": "Erreur lors de la connexion avec {provider}. Cette méthode de connexion n'existe pas.",
|
"Error while login with {provider}. This login provider doesn't exist.": "Erreur lors de la connexion avec {provider}. Cette méthode de connexion n'existe pas.",
|
||||||
|
@ -484,6 +490,7 @@
|
||||||
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
|
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
|
||||||
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
|
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
|
||||||
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
|
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
|
||||||
|
"From the {startDate} to the {endDate} at {endTime}": "Du {startDate} au {endDate} à {endTime}",
|
||||||
"From this instance only": "Depuis cette instance uniquement",
|
"From this instance only": "Depuis cette instance uniquement",
|
||||||
"From yourself": "De vous",
|
"From yourself": "De vous",
|
||||||
"Fully accessible with a wheelchair": "Entièrement accessible avec un fauteuil roulant",
|
"Fully accessible with a wheelchair": "Entièrement accessible avec un fauteuil roulant",
|
||||||
|
@ -527,10 +534,12 @@
|
||||||
"Headline picture": "Image à la une",
|
"Headline picture": "Image à la une",
|
||||||
"Hide filters": "Masquer les filtres",
|
"Hide filters": "Masquer les filtres",
|
||||||
"Hide replies": "Masquer les réponses",
|
"Hide replies": "Masquer les réponses",
|
||||||
|
"Hide the number of participants":"Cacher le nombre de participants",
|
||||||
"Home": "Accueil",
|
"Home": "Accueil",
|
||||||
"Home to {number} users": "Abrite {number} utilisateur·rice·s",
|
"Home to {number} users": "Abrite {number} utilisateur·rice·s",
|
||||||
"Homepage": "Page d'accueil",
|
"Homepage": "Page d'accueil",
|
||||||
"Hourly email summary": "E-mail récapitulatif chaque heure",
|
"Hourly email summary": "E-mail récapitulatif chaque heure",
|
||||||
|
"How to register":"Gestion des participants",
|
||||||
"I agree to the {instanceRules} and {termsOfService}": "J'accepte les {instanceRules} et les {termsOfService}",
|
"I agree to the {instanceRules} and {termsOfService}": "J'accepte les {instanceRules} et les {termsOfService}",
|
||||||
"I create an identity": "Je crée une identité",
|
"I create an identity": "Je crée une identité",
|
||||||
"I don't have a Mobilizon account": "Je n'ai pas de compte Mobilizon",
|
"I don't have a Mobilizon account": "Je n'ai pas de compte Mobilizon",
|
||||||
|
@ -540,7 +549,8 @@
|
||||||
"I participate": "Je participe",
|
"I participate": "Je participe",
|
||||||
"I want to allow people to participate without an account.": "Je veux permettre aux gens de participer sans avoir un compte.",
|
"I want to allow people to participate without an account.": "Je veux permettre aux gens de participer sans avoir un compte.",
|
||||||
"I want to approve every participation request": "Je veux approuver chaque demande de participation",
|
"I want to approve every participation request": "Je veux approuver chaque demande de participation",
|
||||||
"I want to manage the registration with an external provider": "Je souhaite gérer l'enregistrement auprès d'un fournisseur externe",
|
"I want to manage the registration on Mobilizon": "Je souhaite gérer les inscriptions avec Mobilizon",
|
||||||
|
"I want to manage the registration with an external provider": "Je souhaite gérer les inscriptions auprès d'un fournisseur externe",
|
||||||
"I've been mentionned in a comment under an event": "J'ai été mentionné·e dans un commentaire sous un événement",
|
"I've been mentionned in a comment under an event": "J'ai été mentionné·e dans un commentaire sous un événement",
|
||||||
"I've been mentionned in a conversation": "J'ai été mentionnée dans une conversation",
|
"I've been mentionned in a conversation": "J'ai été mentionnée dans une conversation",
|
||||||
"I've been mentionned in a group discussion": "J'ai été mentionné·e dans une discussion d'un groupe",
|
"I've been mentionned in a group discussion": "J'ai été mentionné·e dans une discussion d'un groupe",
|
||||||
|
@ -553,10 +563,10 @@
|
||||||
"Identity {displayName} deleted": "Identité {displayName} supprimée",
|
"Identity {displayName} deleted": "Identité {displayName} supprimée",
|
||||||
"Identity {displayName} updated": "Identité {displayName} mise à jour",
|
"Identity {displayName} updated": "Identité {displayName} mise à jour",
|
||||||
"If allowed by organizer": "Si autorisé par l'organisateur·rice",
|
"If allowed by organizer": "Si autorisé par l'organisateur·rice",
|
||||||
"If an account with this email exists, we just sent another confirmation email to {email}": "Si un compte avec un tel email existe, nous venons juste d'envoyer un nouvel email de confirmation à {email}",
|
"If an account with this email exists, we just sent another confirmation email to {email}": "Si un compte avec un tel e-mail existe, nous venons juste d'envoyer un nouvel e-mail de confirmation à {email}",
|
||||||
"If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "Si cette identité est la seule administratrice de certains groupes, vous devez les supprimer avant de pouvoir supprimer cette identité.",
|
"If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "Si cette identité est la seule administratrice de certains groupes, vous devez les supprimer avant de pouvoir supprimer cette identité.",
|
||||||
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :",
|
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :",
|
||||||
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un email pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.",
|
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un e-mail pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.",
|
||||||
"If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur·ice de l'événement ci-dessous.",
|
"If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur·ice de l'événement ci-dessous.",
|
||||||
"Ignore": "Ignorer",
|
"Ignore": "Ignorer",
|
||||||
"Illustration picture for “{category}” by {author} on {source} ({license})": "Image d'illustration pour “{category}” par {author} sur {source} ({license})",
|
"Illustration picture for “{category}” by {author} on {source} ({license})": "Image d'illustration pour “{category}” par {author} sur {source} ({license})",
|
||||||
|
@ -565,6 +575,8 @@
|
||||||
"In the past": "Dans le passé",
|
"In the past": "Dans le passé",
|
||||||
"In this instance's network": "Dans le réseau de cette instance",
|
"In this instance's network": "Dans le réseau de cette instance",
|
||||||
"Increase": "Augmenter",
|
"Increase": "Augmenter",
|
||||||
|
"Increasing creation date": "Date de création croissante",
|
||||||
|
"Increasing number of members": "Nombre croissant de membres",
|
||||||
"Instance": "Instance",
|
"Instance": "Instance",
|
||||||
"Instance Long Description": "Description longue de l'instance",
|
"Instance Long Description": "Description longue de l'instance",
|
||||||
"Instance Name": "Nom de l'instance",
|
"Instance Name": "Nom de l'instance",
|
||||||
|
@ -605,6 +617,7 @@
|
||||||
"Keyword, event title, group name, etc.": "Mot clé, titre d'un événement, nom d'un groupe, etc.",
|
"Keyword, event title, group name, etc.": "Mot clé, titre d'un événement, nom d'un groupe, etc.",
|
||||||
"Language": "Langue",
|
"Language": "Langue",
|
||||||
"Languages": "Langues",
|
"Languages": "Langues",
|
||||||
|
"Last event activity": "Événements récents",
|
||||||
"Last IP adress": "Dernière adresse IP",
|
"Last IP adress": "Dernière adresse IP",
|
||||||
"Last group created": "Dernier groupe créé",
|
"Last group created": "Dernier groupe créé",
|
||||||
"Last published event": "Dernier événement publié",
|
"Last published event": "Dernier événement publié",
|
||||||
|
@ -633,8 +646,10 @@
|
||||||
"Live": "Direct",
|
"Live": "Direct",
|
||||||
"Load more": "Voir plus",
|
"Load more": "Voir plus",
|
||||||
"Load more activities": "Charger plus d'activités",
|
"Load more activities": "Charger plus d'activités",
|
||||||
|
"Loading…": "Chargement…",
|
||||||
"Loading comments…": "Chargement des commentaires…",
|
"Loading comments…": "Chargement des commentaires…",
|
||||||
"Loading map": "Chargement de la carte",
|
"Loading map": "Chargement de la carte",
|
||||||
|
"Loading search results...": "Chargement des résultats...",
|
||||||
"Local": "Local·e",
|
"Local": "Local·e",
|
||||||
"Local time ({timezone})": "Heure locale ({timezone})",
|
"Local time ({timezone})": "Heure locale ({timezone})",
|
||||||
"Local times ({timezone})": "Heures locales ({timezone})",
|
"Local times ({timezone})": "Heures locales ({timezone})",
|
||||||
|
@ -682,7 +697,7 @@
|
||||||
"Mobilizon software": "logiciel Mobilizon",
|
"Mobilizon software": "logiciel Mobilizon",
|
||||||
"Mobilizon uses a system of profiles to compartiment your activities. You will be able to create as many profiles as you want.": "Mobilizon utilise un système de profils pour compartimenter vos activités. Vous pourrez créer autant de profils que vous voulez.",
|
"Mobilizon uses a system of profiles to compartiment your activities. You will be able to create as many profiles as you want.": "Mobilizon utilise un système de profils pour compartimenter vos activités. Vous pourrez créer autant de profils que vous voulez.",
|
||||||
"Mobilizon version": "Version de Mobilizon",
|
"Mobilizon version": "Version de Mobilizon",
|
||||||
"Mobilizon will send you an email when the events you are attending have important changes: date and time, address, confirmation or cancellation, etc.": "Mobilizon vous enverra un email lors de changements importants pour les événements auxquels vous participez : date et heure, adresse, confirmation ou annulation, etc.",
|
"Mobilizon will send you an email when the events you are attending have important changes: date and time, address, confirmation or cancellation, etc.": "Mobilizon vous enverra un e-mail lors de changements importants pour les événements auxquels vous participez : date et heure, adresse, confirmation ou annulation, etc.",
|
||||||
"Moderate new members": "Modérer les nouvelles et nouveaux membres",
|
"Moderate new members": "Modérer les nouvelles et nouveaux membres",
|
||||||
"Moderated comments (shown after approval)": "Commentaires modérés (affichés après validation)",
|
"Moderated comments (shown after approval)": "Commentaires modérés (affichés après validation)",
|
||||||
"Moderation": "Modération",
|
"Moderation": "Modération",
|
||||||
|
@ -720,7 +735,8 @@
|
||||||
"Next month": "Le mois-prochain",
|
"Next month": "Le mois-prochain",
|
||||||
"Next page": "Page suivante",
|
"Next page": "Page suivante",
|
||||||
"Next week": "La semaine prochaine",
|
"Next week": "La semaine prochaine",
|
||||||
"No activities found": "Aucun activité trouvé",
|
"No about content yet":"À propos n'est pas encore renseigné",
|
||||||
|
"No activities found": "Aucune activité trouvée",
|
||||||
"No address defined": "Aucune adresse définie",
|
"No address defined": "Aucune adresse définie",
|
||||||
"No apps authorized yet": "Aucune application autorisée pour le moment",
|
"No apps authorized yet": "Aucune application autorisée pour le moment",
|
||||||
"No categories with public upcoming events on this instance were found.": "Aucune catégorie avec des événements publics à venir n'a été trouvée.",
|
"No categories with public upcoming events on this instance were found.": "Aucune catégorie avec des événements publics à venir n'a été trouvée.",
|
||||||
|
@ -747,12 +763,13 @@
|
||||||
"No instance to remove|Remove instance|Remove {number} instances": "Pas d'instances à supprimer|Supprimer une instance|Supprimer {number} instances",
|
"No instance to remove|Remove instance|Remove {number} instances": "Pas d'instances à supprimer|Supprimer une instance|Supprimer {number} instances",
|
||||||
"No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?",
|
"No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?",
|
||||||
"No languages found": "Aucune langue trouvée",
|
"No languages found": "Aucune langue trouvée",
|
||||||
|
"No location yet":"Localisation non renseignée",
|
||||||
"No member matches the filters": "Aucun·e membre ne correspond aux filtres",
|
"No member matches the filters": "Aucun·e membre ne correspond aux filtres",
|
||||||
"No members found": "Aucun·e membre trouvé·e",
|
"No members found": "Aucun·e membre trouvé·e",
|
||||||
"No memberships found": "Aucune adhésion trouvée",
|
"No memberships found": "Aucune adhésion trouvée",
|
||||||
"No message": "Pas de message",
|
"No message": "Pas de message",
|
||||||
"No moderation logs yet": "Pas encore de journaux de modération",
|
"No moderation logs yet": "Pas encore de journaux de modération",
|
||||||
"No more activity to display.": "Il n'y a plus d'activités à afficher.",
|
"No more activity to display.": "Il n'y a plus d'activité à afficher.",
|
||||||
"No one is participating|One person participating|{going} people participating": "Personne ne participe|Une personne participe|{going} personnes participent",
|
"No one is participating|One person participating|{going} people participating": "Personne ne participe|Une personne participe|{going} personnes participent",
|
||||||
"No open reports yet": "Aucun signalement ouvert pour le moment",
|
"No open reports yet": "Aucun signalement ouvert pour le moment",
|
||||||
"No organized events found": "Aucun événement organisé trouvé",
|
"No organized events found": "Aucun événement organisé trouvé",
|
||||||
|
@ -764,6 +781,7 @@
|
||||||
"No posts found": "Aucun billet trouvé",
|
"No posts found": "Aucun billet trouvé",
|
||||||
"No posts yet": "Pas encore de billets",
|
"No posts yet": "Pas encore de billets",
|
||||||
"No profile matches the filters": "Aucun profil ne correspond aux filtres",
|
"No profile matches the filters": "Aucun profil ne correspond aux filtres",
|
||||||
|
"No public upcoming activities": "Aucune activité publique à venir",
|
||||||
"No public upcoming events": "Aucun événement public à venir",
|
"No public upcoming events": "Aucun événement public à venir",
|
||||||
"No resolved reports yet": "Aucun signalement résolu pour le moment",
|
"No resolved reports yet": "Aucun signalement résolu pour le moment",
|
||||||
"No resources in this folder": "Aucune ressource dans ce dossier",
|
"No resources in this folder": "Aucune ressource dans ce dossier",
|
||||||
|
@ -796,7 +814,7 @@
|
||||||
"On foot": "À pied",
|
"On foot": "À pied",
|
||||||
"On the Fediverse": "Dans le fediverse",
|
"On the Fediverse": "Dans le fediverse",
|
||||||
"On {date}": "Le {date}",
|
"On {date}": "Le {date}",
|
||||||
"On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}",
|
"On {date} ending at {endTime}": "Le {date} jusqu'à {endTime}",
|
||||||
"On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
|
"On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
|
||||||
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
|
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
|
||||||
"On {instance} and other federated instances": "Sur {instance} et d'autres instances fédérées",
|
"On {instance} and other federated instances": "Sur {instance} et d'autres instances fédérées",
|
||||||
|
@ -847,7 +865,7 @@
|
||||||
"Participants": "Participant·e·s",
|
"Participants": "Participant·e·s",
|
||||||
"Participants to {eventTitle}": "Participant·es à {eventTitle}",
|
"Participants to {eventTitle}": "Participant·es à {eventTitle}",
|
||||||
"Participate": "Participer",
|
"Participate": "Participer",
|
||||||
"Participate using your email address": "Participer en utilisant votre adresse email",
|
"Participate using your email address": "Participer en utilisant votre adresse e-mail",
|
||||||
"Participation approval": "Validation des participations",
|
"Participation approval": "Validation des participations",
|
||||||
"Participation confirmation": "Confirmation de votre participation",
|
"Participation confirmation": "Confirmation de votre participation",
|
||||||
"Participation notifications": "Notifications de participation",
|
"Participation notifications": "Notifications de participation",
|
||||||
|
@ -869,7 +887,7 @@
|
||||||
"Pick an identity": "Choisissez une identité",
|
"Pick an identity": "Choisissez une identité",
|
||||||
"Pick an instance": "Choisir une instance",
|
"Pick an instance": "Choisir une instance",
|
||||||
"Please add as many details as possible to help identify the problem.": "Merci d'ajouter un maximum de détails afin d'aider à identifier le problème.",
|
"Please add as many details as possible to help identify the problem.": "Merci d'ajouter un maximum de détails afin d'aider à identifier le problème.",
|
||||||
"Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.",
|
"Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'e-mail.",
|
||||||
"Please contact this instance's Mobilizon admin if you think this is a mistake.": "Veuillez contacter l'administrateur·rice de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.",
|
"Please contact this instance's Mobilizon admin if you think this is a mistake.": "Veuillez contacter l'administrateur·rice de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.",
|
||||||
"Please do not use it in any real way.": "Merci de ne pas en faire une utilisation réelle.",
|
"Please do not use it in any real way.": "Merci de ne pas en faire une utilisation réelle.",
|
||||||
"Please enter your password to confirm this action.": "Merci d'entrer votre mot de passe pour confirmer cette action.",
|
"Please enter your password to confirm this action.": "Merci d'entrer votre mot de passe pour confirmer cette action.",
|
||||||
|
@ -890,7 +908,7 @@
|
||||||
"Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.": "Propulsé par {mobilizon}. © 2018 - {date} Les contributeur·rice·s Mobilizon - Fait avec le soutien financier de {contributors}.",
|
"Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.": "Propulsé par {mobilizon}. © 2018 - {date} Les contributeur·rice·s Mobilizon - Fait avec le soutien financier de {contributors}.",
|
||||||
"Preferences": "Préférences",
|
"Preferences": "Préférences",
|
||||||
"Previous": "Précédent",
|
"Previous": "Précédent",
|
||||||
"Previous email": "Email précédent",
|
"Previous email": "E-mail précédent",
|
||||||
"Previous month": "Mois précédent",
|
"Previous month": "Mois précédent",
|
||||||
"Previous page": "Page précédente",
|
"Previous page": "Page précédente",
|
||||||
"Price sheet": "Feuille des prix",
|
"Price sheet": "Feuille des prix",
|
||||||
|
@ -985,8 +1003,8 @@
|
||||||
"Reports": "Signalements",
|
"Reports": "Signalements",
|
||||||
"Reports list": "Liste des signalements",
|
"Reports list": "Liste des signalements",
|
||||||
"Request for participation confirmation sent": "Demande de confirmation de participation envoyée",
|
"Request for participation confirmation sent": "Demande de confirmation de participation envoyée",
|
||||||
"Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
|
"Resend confirmation email": "Envoyer à nouveau l'e-mail de confirmation",
|
||||||
"Resent confirmation email": "Réenvoi de l'email de confirmation",
|
"Resent confirmation email": "Réenvoi de l'e-mail de confirmation",
|
||||||
"Reset": "Remettre à zéro",
|
"Reset": "Remettre à zéro",
|
||||||
"Reset filters": "Réinitialiser les filtres",
|
"Reset filters": "Réinitialiser les filtres",
|
||||||
"Reset my password": "Réinitialiser mon mot de passe",
|
"Reset my password": "Réinitialiser mon mot de passe",
|
||||||
|
@ -1016,15 +1034,16 @@
|
||||||
"Select a radius": "Sélectionnez un rayon",
|
"Select a radius": "Sélectionnez un rayon",
|
||||||
"Select a timezone": "Selectionnez un fuseau horaire",
|
"Select a timezone": "Selectionnez un fuseau horaire",
|
||||||
"Select all resources": "Sélectionner toutes les ressources",
|
"Select all resources": "Sélectionner toutes les ressources",
|
||||||
|
"Select distance": "Sélectionner la distance",
|
||||||
"Select languages": "Choisissez une langue",
|
"Select languages": "Choisissez une langue",
|
||||||
"Select the activities for which you wish to receive an email or a push notification.": "Sélectionnez les activités pour lesquelles vous souhaitez recevoir un email ou une notification push.",
|
"Select the activities for which you wish to receive an email or a push notification.": "Sélectionnez les activités pour lesquelles vous souhaitez recevoir un e-mail ou une notification push.",
|
||||||
"Select this resource": "Sélectionner cette ressource",
|
"Select this resource": "Sélectionner cette ressource",
|
||||||
"Send": "Envoyer",
|
"Send": "Envoyer",
|
||||||
"Send email": "Envoyer un email",
|
"Send email": "Envoyer un e-mail",
|
||||||
"Send feedback": "Envoyer vos remarques",
|
"Send feedback": "Envoyer vos remarques",
|
||||||
"Send notification e-mails": "Envoyer des e-mails de notification",
|
"Send notification e-mails": "Envoyer des e-mails de notification",
|
||||||
"Send password reset": "Envoi de la réinitalisation du mot de passe",
|
"Send password reset": "Envoi de la réinitalisation du mot de passe",
|
||||||
"Send the confirmation email again": "Envoyer l'email de confirmation à nouveau",
|
"Send the confirmation email again": "Envoyer l'e-mail de confirmation à nouveau",
|
||||||
"Send the report": "Envoyer le signalement",
|
"Send the report": "Envoyer le signalement",
|
||||||
"Sent to {count} participants": "Envoyé à aucun·e participant·e|Envoyé à une participant·e|Envoyé à {count} participant·es",
|
"Sent to {count} participants": "Envoyé à aucun·e participant·e|Envoyé à une participant·e|Envoyé à {count} participant·es",
|
||||||
"Set an URL to a page with your own privacy policy.": "Entrez une URL vers une page web avec votre propre politique de confidentialité.",
|
"Set an URL to a page with your own privacy policy.": "Entrez une URL vers une page web avec votre propre politique de confidentialité.",
|
||||||
|
@ -1043,6 +1062,7 @@
|
||||||
"Show the time when the event ends": "Afficher l'heure de fin de l'événement",
|
"Show the time when the event ends": "Afficher l'heure de fin de l'événement",
|
||||||
"Showing events before": "Afficher les événements avant",
|
"Showing events before": "Afficher les événements avant",
|
||||||
"Showing events starting on": "Afficher les événements à partir de",
|
"Showing events starting on": "Afficher les événements à partir de",
|
||||||
|
"Showing participants":"Affichage des participants",
|
||||||
"Sign Language": "Langue des signes",
|
"Sign Language": "Langue des signes",
|
||||||
"Sign in with": "Se connecter avec",
|
"Sign in with": "Se connecter avec",
|
||||||
"Sign up": "S'enregistrer",
|
"Sign up": "S'enregistrer",
|
||||||
|
@ -1090,7 +1110,7 @@
|
||||||
"The URL where the event can be watched live": "L'URL où l'événement peut être visionné en direct",
|
"The URL where the event can be watched live": "L'URL où l'événement peut être visionné en direct",
|
||||||
"The URL where the event live can be watched again after it has ended": "L'URL où le direct de l'événement peut être visionné à nouveau une fois terminé",
|
"The URL where the event live can be watched again after it has ended": "L'URL où le direct de l'événement peut être visionné à nouveau une fois terminé",
|
||||||
"The Zoom video teleconference URL": "L'URL de visio-conférence Zoom",
|
"The Zoom video teleconference URL": "L'URL de visio-conférence Zoom",
|
||||||
"The account's email address was changed. Check your emails to verify it.": "L'adresse email du compte a été modifiée. Vérifiez vos emails pour confirmer le changement.",
|
"The account's email address was changed. Check your emails to verify it.": "L'adresse e-mail du compte a été modifiée. Vérifiez vos e-mails pour confirmer le changement.",
|
||||||
"The actual number of participants may differ, as this event is hosted on another instance.": "Le nombre réel de participant·e·s peut être différent, car cet événement provient d'une autre instance.",
|
"The actual number of participants may differ, as this event is hosted on another instance.": "Le nombre réel de participant·e·s peut être différent, car cet événement provient d'une autre instance.",
|
||||||
"The calc will be created on {service}": "Le calc sera créé sur {service}",
|
"The calc will be created on {service}": "Le calc sera créé sur {service}",
|
||||||
"The content came from another server. Transfer an anonymous copy of the report?": "Le contenu provient d'une autre instance. Transférer une copie anonyme du signalement ?",
|
"The content came from another server. Transfer an anonymous copy of the report?": "Le contenu provient d'une autre instance. Transférer une copie anonyme du signalement ?",
|
||||||
|
@ -1161,7 +1181,7 @@
|
||||||
"These events may interest you": "Ces événements peuvent vous intéresser",
|
"These events may interest you": "Ces événements peuvent vous intéresser",
|
||||||
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "Ces flux contiennent des informations sur les événements pour lesquels n'importe lequel de vos profils est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux spécifiques à chaque profil sur la page d'édition des profils.",
|
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "Ces flux contiennent des informations sur les événements pour lesquels n'importe lequel de vos profils est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux spécifiques à chaque profil sur la page d'édition des profils.",
|
||||||
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "Ces flux contiennent des informations sur les événements pour lesquels ce profil spécifique est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux pour l'ensemble de vos profils dans vos paramètres de notification.",
|
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "Ces flux contiennent des informations sur les événements pour lesquels ce profil spécifique est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux pour l'ensemble de vos profils dans vos paramètres de notification.",
|
||||||
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur·ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par email.",
|
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur·ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par e-mail.",
|
||||||
"This URL doesn't seem to be valid": "Cette URL ne semble pas être valide",
|
"This URL doesn't seem to be valid": "Cette URL ne semble pas être valide",
|
||||||
"This URL is not supported": "Cette URL n'est pas supportée",
|
"This URL is not supported": "Cette URL n'est pas supportée",
|
||||||
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "Cette annonce sera envoyée à tous les participant·es ayant le statut sélectionné ci-dessous. Iels ne pourront pas répondre à votre annonce, mais iels peuvent créer une nouvelle conversation avec vous.",
|
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "Cette annonce sera envoyée à tous les participant·es ayant le statut sélectionné ci-dessous. Iels ne pourront pas répondre à votre annonce, mais iels peuvent créer une nouvelle conversation avec vous.",
|
||||||
|
@ -1170,7 +1190,7 @@
|
||||||
"This application will be able to access all of your informations and post content. Make sure you only approve applications you trust.": "Cette application sera capable d'accéder à toutes vos informations et poster du contenu. Assurez-vous d'approuver uniquement des applications en lesquelles vous avez confiance.",
|
"This application will be able to access all of your informations and post content. Make sure you only approve applications you trust.": "Cette application sera capable d'accéder à toutes vos informations et poster du contenu. Assurez-vous d'approuver uniquement des applications en lesquelles vous avez confiance.",
|
||||||
"This application will be allowed to access all of the groups you're a member of": "Cette application pourra accéder à tous les groupes dont vous êtes membres",
|
"This application will be allowed to access all of the groups you're a member of": "Cette application pourra accéder à tous les groupes dont vous êtes membres",
|
||||||
"This application will be allowed to access group activities in all of the groups you're a member of": "This application will be allowed to access group activities in all of the groups you're a member of",
|
"This application will be allowed to access group activities in all of the groups you're a member of": "This application will be allowed to access group activities in all of the groups you're a member of",
|
||||||
"This application will be allowed to access your user activity settings": "Cette application sera autorisée a accéder à vos paramètres utilisateur·ice d'activité",
|
"This application will be allowed to access your user activity settings": "Cette application sera autorisée à accéder à vos paramètres utilisateur·ice d'activité",
|
||||||
"This application will be allowed to access your user settings": "Cette application sera autorisée a accéder à vos paramètres utilisateur·ice",
|
"This application will be allowed to access your user settings": "Cette application sera autorisée a accéder à vos paramètres utilisateur·ice",
|
||||||
"This application will be allowed to create feed tokens": "This application will be allowed to create feed tokens",
|
"This application will be allowed to create feed tokens": "This application will be allowed to create feed tokens",
|
||||||
"This application will be allowed to create group discussions": "This application will be allowed to create group discussions",
|
"This application will be allowed to create group discussions": "This application will be allowed to create group discussions",
|
||||||
|
@ -1248,6 +1268,7 @@
|
||||||
"Time in your timezone ({timezone})": "Heure dans votre fuseau horaire ({timezone})",
|
"Time in your timezone ({timezone})": "Heure dans votre fuseau horaire ({timezone})",
|
||||||
"Times in your timezone ({timezone})": "Heures dans votre fuseau horaire ({timezone})",
|
"Times in your timezone ({timezone})": "Heures dans votre fuseau horaire ({timezone})",
|
||||||
"Timezone": "Fuseau horaire",
|
"Timezone": "Fuseau horaire",
|
||||||
|
"Timezone parameters": "Paramétrer le fuseau horaire",
|
||||||
"Timezone detected as {timezone}.": "Fuseau horaire détecté en tant que {timezone}.",
|
"Timezone detected as {timezone}.": "Fuseau horaire détecté en tant que {timezone}.",
|
||||||
"Title": "Titre",
|
"Title": "Titre",
|
||||||
"To activate more notifications, head over to the notification settings.": "Pour activer plus de notifications, rendez-vous dans vos paramètres de notification.",
|
"To activate more notifications, head over to the notification settings.": "Pour activer plus de notifications, rendez-vous dans vos paramètres de notification.",
|
||||||
|
@ -1321,7 +1342,7 @@
|
||||||
"Username": "Identifiant",
|
"Username": "Identifiant",
|
||||||
"Users": "Utilisateur·rice·s",
|
"Users": "Utilisateur·rice·s",
|
||||||
"Validating account": "Validation du compte",
|
"Validating account": "Validation du compte",
|
||||||
"Validating email": "Validation de l'email",
|
"Validating email": "Validation de l'e-mail",
|
||||||
"Video Conference": "Visio-conférence",
|
"Video Conference": "Visio-conférence",
|
||||||
"View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses",
|
"View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses",
|
||||||
"View account on {hostname} (in a new window)": "Voir le compte sur {hostname} (dans une nouvelle fenêtre)",
|
"View account on {hostname} (in a new window)": "Voir le compte sur {hostname} (dans une nouvelle fenêtre)",
|
||||||
|
@ -1339,6 +1360,7 @@
|
||||||
"View more groups around {position}": "Voir plus de groupes près de {position}",
|
"View more groups around {position}": "Voir plus de groupes près de {position}",
|
||||||
"View more online events": "Voir plus d'événements en ligne",
|
"View more online events": "Voir plus d'événements en ligne",
|
||||||
"View page on {hostname} (in a new window)": "Voir la page sur {hostname} (dans une nouvelle fenêtre)",
|
"View page on {hostname} (in a new window)": "Voir la page sur {hostname} (dans une nouvelle fenêtre)",
|
||||||
|
"View past activities": "Voir les activités passées",
|
||||||
"View past events": "Voir les événements passés",
|
"View past events": "Voir les événements passés",
|
||||||
"View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine",
|
"View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine",
|
||||||
"Visibility was set to an unknown value.": "La visibilité a été définie à une valeur inconnue.",
|
"Visibility was set to an unknown value.": "La visibilité a été définie à une valeur inconnue.",
|
||||||
|
@ -1352,11 +1374,11 @@
|
||||||
"We collect your feedback and the error information in order to improve this service.": "Nous recueillons vos réactions et les informations sur les erreurs afin d'améliorer ce service.",
|
"We collect your feedback and the error information in order to improve this service.": "Nous recueillons vos réactions et les informations sur les erreurs afin d'améliorer ce service.",
|
||||||
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "Nous n'avons pas pu sauvegarder votre participation dans ce navigateur. Aucune inquiétude, vous avez bien confirmé votre participation, nous n'avons juste pas pu enregistrer son statut dans ce navigateur à cause d'un souci technique.",
|
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "Nous n'avons pas pu sauvegarder votre participation dans ce navigateur. Aucune inquiétude, vous avez bien confirmé votre participation, nous n'avons juste pas pu enregistrer son statut dans ce navigateur à cause d'un souci technique.",
|
||||||
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Nous améliorons ce logiciel grâce à vos retours. Pour nous avertir de ce problème, vous avez deux possibilités (les deux requièrent toutefois la création d'un compte) :",
|
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Nous améliorons ce logiciel grâce à vos retours. Pour nous avertir de ce problème, vous avez deux possibilités (les deux requièrent toutefois la création d'un compte) :",
|
||||||
"We just sent an email to {email}": "Nous venons d'envoyer un email à {email}",
|
"We just sent an email to {email}": "Nous venons d'envoyer un e-mail à {email}",
|
||||||
"We use your timezone to make sure you get notifications for an event at the correct time.": "Nous utilisons votre fuseau horaire pour nous assurer que vous recevez les notifications pour un événement au bon moment.",
|
"We use your timezone to make sure you get notifications for an event at the correct time.": "Nous utilisons votre fuseau horaire pour nous assurer que vous recevez les notifications pour un événement au bon moment.",
|
||||||
"We will redirect you to your instance in order to interact with this event": "Nous vous redirigerons vers votre instance pour interagir avec cet événement",
|
"We will redirect you to your instance in order to interact with this event": "Nous vous redirigerons vers votre instance pour interagir avec cet événement",
|
||||||
"We will redirect you to your instance in order to interact with this group": "Nous vous redirigerons vers votre instance afin que vous puissiez interagir avec ce groupe",
|
"We will redirect you to your instance in order to interact with this group": "Nous vous redirigerons vers votre instance afin que vous puissiez interagir avec ce groupe",
|
||||||
"We'll send you an email one hour before the event begins, to be sure you won't forget about it.": "Nous vous enverrons un email une heure avant que l'événement débute, pour être sûr que vous ne l'oubliez pas.",
|
"We'll send you an email one hour before the event begins, to be sure you won't forget about it.": "Nous vous enverrons un e-mail une heure avant que l'événement débute, pour être sûr que vous ne l'oubliez pas.",
|
||||||
"We'll use your timezone settings to send a recap of the morning of the event.": "Nous prendrons en compte votre fuseau horaire pour vous envoyer un récapitulatif de vos événements le matin.",
|
"We'll use your timezone settings to send a recap of the morning of the event.": "Nous prendrons en compte votre fuseau horaire pour vous envoyer un récapitulatif de vos événements le matin.",
|
||||||
"Website": "Site web",
|
"Website": "Site web",
|
||||||
"Website / URL": "Site web / URL",
|
"Website / URL": "Site web / URL",
|
||||||
|
@ -1408,7 +1430,7 @@
|
||||||
"You can try another search term or drag and drop the marker on the map": "Vous pouvez essayer avec d'autres termes de recherche ou bien glisser et déposer le marqueur sur la carte",
|
"You can try another search term or drag and drop the marker on the map": "Vous pouvez essayer avec d'autres termes de recherche ou bien glisser et déposer le marqueur sur la carte",
|
||||||
"You can't change your password because you are registered through {provider}.": "Vous ne pouvez pas changer votre mot de passe car vous vous êtes enregistré via {provider}.",
|
"You can't change your password because you are registered through {provider}.": "Vous ne pouvez pas changer votre mot de passe car vous vous êtes enregistré via {provider}.",
|
||||||
"You can't use push notifications in this browser.": "Vous ne pouvez pas utiliser les notifications push dans ce navigateur.",
|
"You can't use push notifications in this browser.": "Vous ne pouvez pas utiliser les notifications push dans ce navigateur.",
|
||||||
"You changed your email or password": "Vous avez modifié votre email ou votre mot de passe",
|
"You changed your email or password": "Vous avez modifié votre e-mail ou votre mot de passe",
|
||||||
"You created the discussion {discussion}.": "Vous avez créé la discussion {discussion}.",
|
"You created the discussion {discussion}.": "Vous avez créé la discussion {discussion}.",
|
||||||
"You created the event {event}.": "Vous avez créé l'événement {event}.",
|
"You created the event {event}.": "Vous avez créé l'événement {event}.",
|
||||||
"You created the folder {resource}.": "Vous avez créé le dossier {resource}.",
|
"You created the folder {resource}.": "Vous avez créé le dossier {resource}.",
|
||||||
|
@ -1481,7 +1503,7 @@
|
||||||
"You'll get a weekly recap every Monday for upcoming events, if you have any.": "Vous recevrez un récapitulatif hebdomadaire chaque lundi pour les événements de la semaine, si vous en avez.",
|
"You'll get a weekly recap every Monday for upcoming events, if you have any.": "Vous recevrez un récapitulatif hebdomadaire chaque lundi pour les événements de la semaine, si vous en avez.",
|
||||||
"You'll need to change the URLs where there were previously entered.": "Vous devrez changer les URLs là où vous les avez entrées précédemment.",
|
"You'll need to change the URLs where there were previously entered.": "Vous devrez changer les URLs là où vous les avez entrées précédemment.",
|
||||||
"You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "Vous aurez besoin de transmettre l'URL du groupe pour que d'autres personnes accèdent au profil du groupe. Le groupe ne sera pas trouvable dans la recherche de Mobilizon ni dans les moteurs de recherche habituels.",
|
"You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "Vous aurez besoin de transmettre l'URL du groupe pour que d'autres personnes accèdent au profil du groupe. Le groupe ne sera pas trouvable dans la recherche de Mobilizon ni dans les moteurs de recherche habituels.",
|
||||||
"You'll receive a confirmation email.": "Vous recevrez un email de confirmation.",
|
"You'll receive a confirmation email.": "Vous recevrez un e-mail de confirmation.",
|
||||||
"YouTube live": "Direct sur YouTube",
|
"YouTube live": "Direct sur YouTube",
|
||||||
"YouTube replay": "Replay sur YouTube",
|
"YouTube replay": "Replay sur YouTube",
|
||||||
"Your account has been successfully deleted": "Votre compte a été supprimé avec succès",
|
"Your account has been successfully deleted": "Votre compte a été supprimé avec succès",
|
||||||
|
@ -1492,10 +1514,10 @@
|
||||||
"Your city or region and the radius will only be used to suggest you events nearby. The event radius will consider the administrative center of the area.": "Votre ville ou région et le rayon seront uniquement utilisés pour vous suggérer des événements proches. Le rayon des événements proches sera calculé par rapport au centre administratif de la zone.",
|
"Your city or region and the radius will only be used to suggest you events nearby. The event radius will consider the administrative center of the area.": "Votre ville ou région et le rayon seront uniquement utilisés pour vous suggérer des événements proches. Le rayon des événements proches sera calculé par rapport au centre administratif de la zone.",
|
||||||
"Your current email is {email}. You use it to log in.": "Votre adresse e-mail actuelle est {email}. Vous l'utilisez pour vous connecter.",
|
"Your current email is {email}. You use it to log in.": "Votre adresse e-mail actuelle est {email}. Vous l'utilisez pour vous connecter.",
|
||||||
"Your email": "Votre adresse e-mail",
|
"Your email": "Votre adresse e-mail",
|
||||||
"Your email address was automatically set based on your {provider} account.": "Votre adresse email a été définie automatiquement en se basant sur votre compte {provider}.",
|
"Your email address was automatically set based on your {provider} account.": "Votre adresse e-mail a été définie automatiquement en se basant sur votre compte {provider}.",
|
||||||
"Your email has been changed": "Votre adresse email a bien été modifiée",
|
"Your email has been changed": "Votre adresse e-mail a bien été modifiée",
|
||||||
"Your email is being changed": "Votre adresse email est en train d'être modifiée",
|
"Your email is being changed": "Votre adresse e-mail est en train d'être modifiée",
|
||||||
"Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Votre email sera uniquement utilisé pour confirmer que vous êtes bien une personne réelle et vous envoyer des éventuelles mises à jour pour cet événement. Il ne sera PAS transmis à d'autres instances ou à l'organisateur de l'événement.",
|
"Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Votre e-mail sera uniquement utilisé pour confirmer que vous êtes bien une personne réelle et vous envoyer des éventuelles mises à jour pour cet événement. Il ne sera PAS transmis à d'autres instances ou à l'organisateur de l'événement.",
|
||||||
"Your federated identity": "Votre identité fédérée",
|
"Your federated identity": "Votre identité fédérée",
|
||||||
"Your membership is pending approval": "Votre adhésion est en attente d'approbation",
|
"Your membership is pending approval": "Votre adhésion est en attente d'approbation",
|
||||||
"Your membership was approved by {profile}.": "Votre demande d'adhésion a été approuvée par {profile}.",
|
"Your membership was approved by {profile}.": "Votre demande d'adhésion a été approuvée par {profile}.",
|
||||||
|
@ -1531,6 +1553,7 @@
|
||||||
"as {identity}": "en tant que {identity}",
|
"as {identity}": "en tant que {identity}",
|
||||||
"contact uninformed": "contact non renseigné",
|
"contact uninformed": "contact non renseigné",
|
||||||
"create a group": "créer un groupe",
|
"create a group": "créer un groupe",
|
||||||
|
"create an activity": "créer une activité",
|
||||||
"create an event": "créer un événement",
|
"create an event": "créer un événement",
|
||||||
"default Mobilizon privacy policy": "politique de confidentialité par défaut de Mobilizon",
|
"default Mobilizon privacy policy": "politique de confidentialité par défaut de Mobilizon",
|
||||||
"default Mobilizon terms": "conditions d'utilisation par défaut de Mobilizon",
|
"default Mobilizon terms": "conditions d'utilisation par défaut de Mobilizon",
|
||||||
|
@ -1562,14 +1585,14 @@
|
||||||
"{'@'}{username}": "{'@'}{username}",
|
"{'@'}{username}": "{'@'}{username}",
|
||||||
"{'@'}{username} ({role})": "{'@'}{username} ({role})",
|
"{'@'}{username} ({role})": "{'@'}{username} ({role})",
|
||||||
"{approved} / {total} seats": "{approved} / {total} places",
|
"{approved} / {total} seats": "{approved} / {total} places",
|
||||||
"{available}/{capacity} available places": "Pas de places restantes|{available}/{capacity} places restantes|{available}/{capacity} places restantes",
|
"{available}/{capacity} available places": "Pas de places restantes|{available}/{capacity} place restante|{available}/{capacity} places restantes",
|
||||||
"{count} events": "{count} événements",
|
"{count} events": "{count} événements",
|
||||||
"{count} km": "{count} km",
|
"{count} km": "{count} km",
|
||||||
"{count} members": "Aucun membre|Un·e membre|{count} membres",
|
"{count} members": "Aucun membre|Un·e membre|{count} membres",
|
||||||
"{count} members or followers": "Aucun·e membre ou abonné·e|Un·e membre ou abonné·e|{count} membres ou abonné·es",
|
"{count} members or followers": "Aucun·e membre ou abonné·e|Un·e membre ou abonné·e|{count} membres ou abonné·es",
|
||||||
"{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s",
|
"{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s",
|
||||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||||
"{eventsCount} activities found": "Aucune activité trouvé|Une activité trouvé|{eventsCount} activités trouvés",
|
"{eventsCount} activities found": "Aucune activité trouvée|Une activité trouvée|{eventsCount} activités trouvées",
|
||||||
"{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés",
|
"{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés",
|
||||||
"{folder} - Resources": "{folder} - Ressources",
|
"{folder} - Resources": "{folder} - Ressources",
|
||||||
"{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés",
|
"{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés",
|
||||||
|
@ -1608,7 +1631,7 @@
|
||||||
"{number} organized events": "Aucun événement organisé|Un événement organisé|{number} événements organisés",
|
"{number} organized events": "Aucun événement organisé|Un événement organisé|{number} événements organisés",
|
||||||
"{number} participations": "Aucune participation|Une participation|{number} participations",
|
"{number} participations": "Aucune participation|Une participation|{number} participations",
|
||||||
"{number} posts": "Aucun billet|Un billet|{number} billets",
|
"{number} posts": "Aucun billet|Un billet|{number} billets",
|
||||||
"{number} seats left": "{number} places restantes",
|
"{number} seats left": "Aucune place restante|Une place restante|{number} places restantes",
|
||||||
"{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.",
|
"{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.",
|
||||||
"{profileName} (suspended)": "{profileName} (suspendu·e)",
|
"{profileName} (suspended)": "{profileName} (suspendu·e)",
|
||||||
"{profile} (by default)": "{profile} (par défault)",
|
"{profile} (by default)": "{profile} (par défault)",
|
||||||
|
|
|
@ -132,9 +132,7 @@ export enum SearchTabs {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContentType {
|
export enum ContentType {
|
||||||
ALL = "ALL",
|
|
||||||
EVENTS = "EVENTS",
|
EVENTS = "EVENTS",
|
||||||
SHORTEVENTS = "SHORTEVENTS",
|
|
||||||
LONGEVENTS = "LONGEVENTS",
|
LONGEVENTS = "LONGEVENTS",
|
||||||
GROUPS = "GROUPS",
|
GROUPS = "GROUPS",
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ export interface IEvent {
|
||||||
beginsOn: string;
|
beginsOn: string;
|
||||||
endsOn: string | null;
|
endsOn: string | null;
|
||||||
publishAt: string;
|
publishAt: string;
|
||||||
|
longEvent: boolean;
|
||||||
status: EventStatus;
|
status: EventStatus;
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
joinOptions: EventJoinOptions;
|
joinOptions: EventJoinOptions;
|
||||||
|
@ -144,6 +145,8 @@ export class EventModel implements IEvent {
|
||||||
|
|
||||||
publishAt = new Date().toISOString();
|
publishAt = new Date().toISOString();
|
||||||
|
|
||||||
|
longEvent = false;
|
||||||
|
|
||||||
language = "und";
|
language = "und";
|
||||||
|
|
||||||
participantStats = {
|
participantStats = {
|
||||||
|
|
|
@ -9,9 +9,19 @@ const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway
|
||||||
export const addressToLocation = (
|
export const addressToLocation = (
|
||||||
address: IAddress
|
address: IAddress
|
||||||
): LocationType | undefined => {
|
): LocationType | undefined => {
|
||||||
if (!address.geom) return undefined;
|
if (!address.geom)
|
||||||
|
return {
|
||||||
|
lon: undefined,
|
||||||
|
lat: undefined,
|
||||||
|
name: undefined,
|
||||||
|
};
|
||||||
const arr = address.geom.split(";");
|
const arr = address.geom.split(";");
|
||||||
if (arr.length < 2) return undefined;
|
if (arr.length < 2)
|
||||||
|
return {
|
||||||
|
lon: undefined,
|
||||||
|
lat: undefined,
|
||||||
|
name: undefined,
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
lon: parseFloat(arr[0]),
|
lon: parseFloat(arr[0]),
|
||||||
lat: parseFloat(arr[1]),
|
lat: parseFloat(arr[1]),
|
||||||
|
@ -19,6 +29,17 @@ export const addressToLocation = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const locationToAddress = (location: LocationType): IAddress | null => {
|
||||||
|
if (location.lon && location.lat) {
|
||||||
|
const new_add = new Address();
|
||||||
|
new_add.geom = location.lon.toString() + ";" + location.lat.toString();
|
||||||
|
new_add.description = location.name || "";
|
||||||
|
console.debug("locationToAddress", location, new_add);
|
||||||
|
return new_add;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const coordsToGeoHash = (
|
export const coordsToGeoHash = (
|
||||||
lat: number | undefined,
|
lat: number | undefined,
|
||||||
lon: number | undefined,
|
lon: number | undefined,
|
||||||
|
@ -38,44 +59,24 @@ export const geoHashToCoords = (
|
||||||
return latitude && longitude ? { latitude, longitude } : undefined;
|
return latitude && longitude ? { latitude, longitude } : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storeLocationInLocal = (location: IAddress | null): undefined => {
|
export const storeAddressInLocal = (address: IAddress | null): undefined => {
|
||||||
if (location) {
|
if (address) {
|
||||||
window.localStorage.setItem("location", JSON.stringify(location));
|
window.localStorage.setItem("address", JSON.stringify(address));
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.removeItem("location");
|
window.localStorage.removeItem("address");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLocationFromLocal = (): IAddress | null => {
|
export const getAddressFromLocal = (): IAddress | null => {
|
||||||
const locationString = window.localStorage.getItem("location");
|
const addressString = window.localStorage.getItem("address");
|
||||||
if (!locationString) {
|
if (!addressString) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const location = JSON.parse(locationString) as IAddress;
|
const address = JSON.parse(addressString) as IAddress;
|
||||||
if (!location.description || !location.geom) {
|
if (!address.description || !address.geom) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return location;
|
return address;
|
||||||
};
|
|
||||||
|
|
||||||
export const storeRadiusInLocal = (radius: number | null): undefined => {
|
|
||||||
if (radius) {
|
|
||||||
window.localStorage.setItem("radius", radius.toString());
|
|
||||||
} else {
|
|
||||||
window.localStorage.removeItem("radius");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRadiusFromLocal = (): IAddress | null => {
|
|
||||||
const locationString = window.localStorage.getItem("location");
|
|
||||||
if (!locationString) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const location = JSON.parse(locationString) as IAddress;
|
|
||||||
if (!location.description || !location.geom) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return location;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storeUserLocationAndRadiusFromUserSettings = (
|
export const storeUserLocationAndRadiusFromUserSettings = (
|
||||||
|
@ -84,18 +85,13 @@ export const storeUserLocationAndRadiusFromUserSettings = (
|
||||||
if (location) {
|
if (location) {
|
||||||
const latlon = geoHashToCoords(location.geohash);
|
const latlon = geoHashToCoords(location.geohash);
|
||||||
if (latlon) {
|
if (latlon) {
|
||||||
storeLocationInLocal({
|
storeAddressInLocal({
|
||||||
...new Address(),
|
...new Address(),
|
||||||
geom: `${latlon.longitude};${latlon.latitude}`,
|
geom: `${latlon.longitude};${latlon.latitude}`,
|
||||||
description: location.name || "",
|
description: location.name || "",
|
||||||
type: "administrative",
|
type: "administrative",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (location.range) {
|
|
||||||
storeRadiusInLocal(location.range);
|
|
||||||
} else {
|
|
||||||
console.debug("user has not set a radius");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.debug("user has not set a location");
|
console.debug("user has not set a location");
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,56 +61,46 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<o-field
|
<o-field
|
||||||
horizontal
|
grouped
|
||||||
|
groupMultiline
|
||||||
:label="t('Starts on…')"
|
:label="t('Starts on…')"
|
||||||
class="items-center"
|
class="items-center"
|
||||||
label-for="begins-on-field"
|
label-for="begins-on-field"
|
||||||
>
|
>
|
||||||
<o-datetimepicker
|
<event-date-picker
|
||||||
class="datepicker starts-on"
|
:time="showStartTime"
|
||||||
:placeholder="t('Type or select a date…')"
|
|
||||||
icon="calendar-today"
|
|
||||||
:locale="$i18n.locale.replace('_', '-')"
|
|
||||||
v-model="beginsOn"
|
v-model="beginsOn"
|
||||||
horizontal-time-picker
|
@blur="consistencyBeginsOnBeforeEndsOn"
|
||||||
:tz-offset="tzOffset(beginsOn)"
|
></event-date-picker>
|
||||||
:first-day-of-week="firstDayOfWeek"
|
<div class="my-2">
|
||||||
:datepicker="{
|
<o-switch v-model="showStartTime">{{
|
||||||
id: 'begins-on-field',
|
t("Show the time when the event begins")
|
||||||
'aria-next-label': t('Next month'),
|
}}</o-switch>
|
||||||
'aria-previous-label': t('Previous month'),
|
</div>
|
||||||
}"
|
|
||||||
>
|
|
||||||
</o-datetimepicker>
|
|
||||||
</o-field>
|
</o-field>
|
||||||
|
|
||||||
<o-field
|
<o-field
|
||||||
horizontal
|
grouped
|
||||||
|
groupMultiline
|
||||||
:label="t('Ends on…')"
|
:label="t('Ends on…')"
|
||||||
label-for="ends-on-field"
|
label-for="ends-on-field"
|
||||||
class="items-center"
|
class="items-center"
|
||||||
>
|
>
|
||||||
<o-datetimepicker
|
<event-date-picker
|
||||||
class="datepicker ends-on"
|
:time="showEndTime"
|
||||||
:placeholder="t('Type or select a date…')"
|
|
||||||
icon="calendar-today"
|
|
||||||
:locale="$i18n.locale.replace('_', '-')"
|
|
||||||
v-model="endsOn"
|
v-model="endsOn"
|
||||||
horizontal-time-picker
|
@blur="consistencyBeginsOnBeforeEndsOn"
|
||||||
:min-datetime="beginsOn"
|
:min="beginsOn"
|
||||||
:tz-offset="tzOffset(endsOn)"
|
></event-date-picker>
|
||||||
:first-day-of-week="firstDayOfWeek"
|
<div class="my-2">
|
||||||
:datepicker="{
|
<o-switch v-model="showEndTime">{{
|
||||||
id: 'ends-on-field',
|
t("Show the time when the event ends")
|
||||||
'aria-next-label': t('Next month'),
|
}}</o-switch>
|
||||||
'aria-previous-label': t('Previous month'),
|
</div>
|
||||||
}"
|
|
||||||
>
|
|
||||||
</o-datetimepicker>
|
|
||||||
</o-field>
|
</o-field>
|
||||||
|
|
||||||
<o-button class="block" variant="text" @click="dateSettingsIsOpen = true">
|
<o-button class="block" variant="text" @click="dateSettingsIsOpen = true">
|
||||||
{{ t("Date parameters") }}
|
{{ t("Timezone parameters") }}
|
||||||
</o-button>
|
</o-button>
|
||||||
|
|
||||||
<div class="my-6">
|
<div class="my-6">
|
||||||
|
@ -396,7 +386,7 @@
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
<h2>{{ t("Status") }}</h2>
|
<h2>{{ t("Status") }}</h2>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset id="status">
|
||||||
<legend>
|
<legend>
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
|
@ -476,7 +466,7 @@
|
||||||
>
|
>
|
||||||
<form class="p-3">
|
<form class="p-3">
|
||||||
<header class="">
|
<header class="">
|
||||||
<h2 class="">{{ t("Date and time settings") }}</h2>
|
<h2 class="">{{ t("Timezone") }}</h2>
|
||||||
</header>
|
</header>
|
||||||
<section class="">
|
<section class="">
|
||||||
<p>
|
<p>
|
||||||
|
@ -486,7 +476,7 @@
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<o-field :label="t('Timezone')" label-for="timezone" expanded>
|
<o-field expanded>
|
||||||
<o-select
|
<o-select
|
||||||
:placeholder="t('Select a timezone')"
|
:placeholder="t('Select a timezone')"
|
||||||
:loading="timezoneLoading"
|
:loading="timezoneLoading"
|
||||||
|
@ -517,16 +507,6 @@
|
||||||
:title="t('Clear timezone field')"
|
:title="t('Clear timezone field')"
|
||||||
/>
|
/>
|
||||||
</o-field>
|
</o-field>
|
||||||
<o-field :label="t('Event page settings')">
|
|
||||||
<o-switch v-model="eventOptions.showStartTime">{{
|
|
||||||
t("Show the time when the event begins")
|
|
||||||
}}</o-switch>
|
|
||||||
</o-field>
|
|
||||||
<o-field>
|
|
||||||
<o-switch v-model="eventOptions.showEndTime">{{
|
|
||||||
t("Show the time when the event ends")
|
|
||||||
}}</o-switch>
|
|
||||||
</o-field>
|
|
||||||
</section>
|
</section>
|
||||||
<footer class="mt-2">
|
<footer class="mt-2">
|
||||||
<o-button @click="dateSettingsIsOpen = false">
|
<o-button @click="dateSettingsIsOpen = false">
|
||||||
|
@ -544,40 +524,45 @@
|
||||||
v-if="hasCurrentActorPermissionsToEdit"
|
v-if="hasCurrentActorPermissionsToEdit"
|
||||||
>
|
>
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<div class="flex justify-between items-center">
|
<div class="lg:flex lg:justify-between lg:items-center lg:flex-wrap">
|
||||||
<span class="dark:text-gray-900" v-if="isEventModified">
|
<div
|
||||||
|
class="text-red-900 text-center w-full margin m-1 lg:m-0 lg:w-auto lg:text-left"
|
||||||
|
v-if="isEventModified"
|
||||||
|
>
|
||||||
{{ t("Unsaved changes") }}
|
{{ t("Unsaved changes") }}
|
||||||
</span>
|
</div>
|
||||||
<div class="flex flex-wrap gap-3 items-center justify-end">
|
<div class="flex flex-wrap gap-3 items-center justify-end">
|
||||||
<o-button
|
<o-button
|
||||||
|
expanded
|
||||||
variant="text"
|
variant="text"
|
||||||
@click="confirmGoBack"
|
@click="confirmGoBack"
|
||||||
class="dark:!text-black ml-auto"
|
class="dark:!text-black ml-auto"
|
||||||
>{{ t("Cancel") }}</o-button
|
>{{ t("Cancel") }}</o-button
|
||||||
>
|
>
|
||||||
<!-- If an event has been published we can't make it draft anymore -->
|
<!-- If an event has been published we can't make it draft anymore -->
|
||||||
<span class="" v-if="event.draft === true">
|
<o-button
|
||||||
<o-button
|
v-if="event.draft === true"
|
||||||
variant="primary"
|
expanded
|
||||||
class="!text-black hover:!text-white"
|
variant="primary"
|
||||||
outlined
|
class="!text-black hover:!text-white"
|
||||||
@click="createOrUpdateDraft"
|
outlined
|
||||||
:disabled="saving"
|
@click="createOrUpdateDraft"
|
||||||
>{{ t("Save draft") }}</o-button
|
:disabled="saving"
|
||||||
>
|
:loading="saving"
|
||||||
</span>
|
>{{ t("Save draft") }}</o-button
|
||||||
<span class="ml-auto">
|
>
|
||||||
<o-button
|
<o-button
|
||||||
variant="primary"
|
expanded
|
||||||
:disabled="saving"
|
variant="primary"
|
||||||
@click="createOrUpdatePublish"
|
:disabled="saving"
|
||||||
@keyup.enter="createOrUpdatePublish"
|
:loading="saving"
|
||||||
>
|
@click="createOrUpdatePublish"
|
||||||
<span v-if="isUpdate === false">{{ t("Create my event") }}</span>
|
@keyup.enter="createOrUpdatePublish"
|
||||||
<span v-else-if="event.draft === true">{{ t("Publish") }}</span>
|
>
|
||||||
<span v-else>{{ t("Update my event") }}</span>
|
<span v-if="isUpdate === false">{{ t("Create my event") }}</span>
|
||||||
</o-button>
|
<span v-else-if="event.draft === true">{{ t("Publish") }}</span>
|
||||||
</span>
|
<span v-else>{{ t("Update my event") }}</span>
|
||||||
|
</o-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -649,7 +634,7 @@ import {
|
||||||
useCurrentUserIdentities,
|
useCurrentUserIdentities,
|
||||||
usePersonStatusGroup,
|
usePersonStatusGroup,
|
||||||
} from "@/composition/apollo/actor";
|
} from "@/composition/apollo/actor";
|
||||||
import { useUserSettings } from "@/composition/apollo/user";
|
import { useLoggedUser } from "@/composition/apollo/user";
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
inject,
|
inject,
|
||||||
|
@ -672,16 +657,16 @@ import { Dialog } from "@/plugins/dialog";
|
||||||
import { Notifier } from "@/plugins/notifier";
|
import { Notifier } from "@/plugins/notifier";
|
||||||
import { useHead } from "@/utils/head";
|
import { useHead } from "@/utils/head";
|
||||||
import { useOruga } from "@oruga-ui/oruga-next";
|
import { useOruga } from "@oruga-ui/oruga-next";
|
||||||
import type { Locale } from "date-fns";
|
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
import { escapeHtml } from "@/utils/html";
|
import { escapeHtml } from "@/utils/html";
|
||||||
|
import EventDatePicker from "@/components/Event/EventDatePicker.vue";
|
||||||
|
|
||||||
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
||||||
|
|
||||||
const { eventCategories } = useEventCategories();
|
const { eventCategories } = useEventCategories();
|
||||||
const { anonymousParticipationConfig } = useAnonymousParticipationConfig();
|
const { anonymousParticipationConfig } = useAnonymousParticipationConfig();
|
||||||
const { currentActor } = useCurrentActorClient();
|
const { currentActor } = useCurrentActorClient();
|
||||||
const { loggedUser } = useUserSettings();
|
const { loggedUser } = useLoggedUser();
|
||||||
const { identities } = useCurrentUserIdentities();
|
const { identities } = useCurrentUserIdentities();
|
||||||
|
|
||||||
const { features } = useFeatures();
|
const { features } = useFeatures();
|
||||||
|
@ -724,11 +709,27 @@ const dateSettingsIsOpen = ref(false);
|
||||||
|
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
|
||||||
const initializeEvent = () => {
|
const setEventTimezoneToUserTimezoneIfUnset = () => {
|
||||||
|
if (userTimezone.value && event.value.options.timezone == null) {
|
||||||
|
event.value.options.timezone = userTimezone.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// usefull if the page is loaded from scratch
|
||||||
|
watch(loggedUser, setEventTimezoneToUserTimezoneIfUnset);
|
||||||
|
|
||||||
|
const initializeNewEvent = () => {
|
||||||
|
// usefull if the data is already cached
|
||||||
|
setEventTimezoneToUserTimezoneIfUnset();
|
||||||
|
|
||||||
|
// Default values for beginsOn and endsOn
|
||||||
|
|
||||||
const roundUpTo15Minutes = (time: Date) => {
|
const roundUpTo15Minutes = (time: Date) => {
|
||||||
time.setMilliseconds(Math.round(time.getMilliseconds() / 1000) * 1000);
|
time.setUTCMilliseconds(
|
||||||
time.setSeconds(Math.round(time.getSeconds() / 60) * 60);
|
Math.round(time.getUTCMilliseconds() / 1000) * 1000
|
||||||
time.setMinutes(Math.round(time.getMinutes() / 15) * 15);
|
);
|
||||||
|
time.setUTCSeconds(Math.round(time.getUTCSeconds() / 60) * 60);
|
||||||
|
time.setUTCMinutes(Math.round(time.getUTCMinutes() / 15) * 15);
|
||||||
return time;
|
return time;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -737,8 +738,15 @@ const initializeEvent = () => {
|
||||||
|
|
||||||
end.setUTCHours(now.getUTCHours() + 3);
|
end.setUTCHours(now.getUTCHours() + 3);
|
||||||
|
|
||||||
event.value.beginsOn = now.toISOString();
|
beginsOn.value = now;
|
||||||
event.value.endsOn = end.toISOString();
|
endsOn.value = end;
|
||||||
|
|
||||||
|
// Default values for showStartTime and showEndTime
|
||||||
|
showStartTime.value = false;
|
||||||
|
showEndTime.value = false;
|
||||||
|
|
||||||
|
// Default values for hideParticipants
|
||||||
|
hideParticipants.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const organizerActor = computed({
|
const organizerActor = computed({
|
||||||
|
@ -796,7 +804,7 @@ onMounted(async () => {
|
||||||
pictureFile.value = await buildFileFromIMedia(event.value.picture);
|
pictureFile.value = await buildFileFromIMedia(event.value.picture);
|
||||||
limitedPlaces.value = eventOptions.value.maximumAttendeeCapacity > 0;
|
limitedPlaces.value = eventOptions.value.maximumAttendeeCapacity > 0;
|
||||||
if (!(props.isUpdate || props.isDuplicate)) {
|
if (!(props.isUpdate || props.isDuplicate)) {
|
||||||
initializeEvent();
|
initializeNewEvent();
|
||||||
} else {
|
} else {
|
||||||
event.value = new EventModel({
|
event.value = new EventModel({
|
||||||
...event.value,
|
...event.value,
|
||||||
|
@ -1209,42 +1217,93 @@ const isEventModified = computed((): boolean => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const beginsOn = computed({
|
const showStartTime = computed({
|
||||||
get(): Date | null {
|
get(): boolean {
|
||||||
// if (this.timezone && this.event.beginsOn) {
|
return event.value.options.showStartTime;
|
||||||
// return utcToZonedTime(this.event.beginsOn, this.timezone);
|
|
||||||
// }
|
|
||||||
return event.value.beginsOn ? new Date(event.value.beginsOn) : null;
|
|
||||||
},
|
},
|
||||||
set(newBeginsOn: Date | null) {
|
set(newShowStartTime: boolean) {
|
||||||
event.value.beginsOn = newBeginsOn?.toISOString() ?? null;
|
event.value.options = {
|
||||||
if (!event.value.endsOn || !newBeginsOn) return;
|
...event.value.options,
|
||||||
const dateBeginsOn = new Date(newBeginsOn);
|
showStartTime: newShowStartTime,
|
||||||
const dateEndsOn = new Date(event.value.endsOn);
|
};
|
||||||
let endsOn = new Date(event.value.endsOn);
|
|
||||||
if (dateEndsOn < dateBeginsOn) {
|
|
||||||
endsOn = dateBeginsOn;
|
|
||||||
endsOn.setHours(dateBeginsOn.getHours() + 1);
|
|
||||||
}
|
|
||||||
if (dateEndsOn === dateBeginsOn) {
|
|
||||||
endsOn.setHours(dateEndsOn.getHours() + 1);
|
|
||||||
}
|
|
||||||
event.value.endsOn = endsOn.toISOString();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const endsOn = computed({
|
const showEndTime = computed({
|
||||||
get(): Date | null {
|
get(): boolean {
|
||||||
// if (this.event.endsOn && this.timezone) {
|
return event.value.options.showEndTime;
|
||||||
// return utcToZonedTime(this.event.endsOn, this.timezone);
|
|
||||||
// }
|
|
||||||
return event.value.endsOn ? new Date(event.value.endsOn) : null;
|
|
||||||
},
|
},
|
||||||
set(newEndsOn: Date | null) {
|
set(newshowEndTime: boolean) {
|
||||||
event.value.endsOn = newEndsOn?.toISOString() ?? null;
|
event.value.options = {
|
||||||
|
...event.value.options,
|
||||||
|
showEndTime: newshowEndTime,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const beginsOn = ref(new Date());
|
||||||
|
const endsOn = ref(new Date());
|
||||||
|
|
||||||
|
const updateEventDateRelatedToTimezone = () => {
|
||||||
|
// update event.value.beginsOn taking care of timezone
|
||||||
|
if (beginsOn.value) {
|
||||||
|
const dateBeginsOn = new Date(beginsOn.value.getTime());
|
||||||
|
dateBeginsOn.setUTCMinutes(dateBeginsOn.getUTCMinutes() - tzOffset.value);
|
||||||
|
event.value.beginsOn = dateBeginsOn.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endsOn.value) {
|
||||||
|
// update event.value.endsOn taking care of timezone
|
||||||
|
const dateEndsOn = new Date(endsOn.value.getTime());
|
||||||
|
dateEndsOn.setUTCMinutes(dateEndsOn.getUTCMinutes() - tzOffset.value);
|
||||||
|
event.value.endsOn = dateEndsOn.toISOString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(beginsOn, (newBeginsOn) => {
|
||||||
|
if (!newBeginsOn) {
|
||||||
|
event.value.beginsOn = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// usefull for comparaison
|
||||||
|
newBeginsOn.setUTCSeconds(0);
|
||||||
|
newBeginsOn.setUTCMilliseconds(0);
|
||||||
|
|
||||||
|
// update event.value.beginsOn taking care of timezone
|
||||||
|
updateEventDateRelatedToTimezone();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(endsOn, (newEndsOn) => {
|
||||||
|
if (!newEndsOn) {
|
||||||
|
event.value.endsOn = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// usefull for comparaison
|
||||||
|
newEndsOn.setUTCSeconds(0);
|
||||||
|
newEndsOn.setUTCMilliseconds(0);
|
||||||
|
|
||||||
|
// update event.value.endsOn taking care of timezone
|
||||||
|
updateEventDateRelatedToTimezone();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
For endsOn, we need to check consistencyBeginsOnBeforeEndsOn() at blur
|
||||||
|
because the datetime-local component update itself immediately
|
||||||
|
Ex : your event start at 10:00 and stops at 12:00
|
||||||
|
To type "10" hours, you will first have "1" hours, then "10" hours
|
||||||
|
So you cannot check consistensy in real time, only onBlur because of the moment we falsely have "1:00"
|
||||||
|
*/
|
||||||
|
const consistencyBeginsOnBeforeEndsOn = () => {
|
||||||
|
// Update endsOn to make sure endsOn is later than beginsOn
|
||||||
|
if (endsOn.value && beginsOn.value && endsOn.value <= beginsOn.value) {
|
||||||
|
const newEndsOn = new Date(beginsOn.value);
|
||||||
|
newEndsOn.setUTCHours(beginsOn.value.getUTCHours() + 1);
|
||||||
|
endsOn.value = newEndsOn;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { timezones: rawTimezones, loading: timezoneLoading } = useTimezones();
|
const { timezones: rawTimezones, loading: timezoneLoading } = useTimezones();
|
||||||
|
|
||||||
const timezones = computed((): Record<string, string[]> => {
|
const timezones = computed((): Record<string, string[]> => {
|
||||||
|
@ -1295,25 +1354,44 @@ const timezone = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Timezone specified in user settings
|
||||||
const userTimezone = computed((): string | undefined => {
|
const userTimezone = computed((): string | undefined => {
|
||||||
return loggedUser.value?.settings?.timezone;
|
return loggedUser.value?.settings?.timezone;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
|
// Timezone specified in user settings or local timezone browser if unavailable
|
||||||
const userActualTimezone = computed((): string => {
|
const userActualTimezone = computed((): string => {
|
||||||
if (userTimezone.value) {
|
if (userTimezone.value) {
|
||||||
return userTimezone.value;
|
return userTimezone.value;
|
||||||
}
|
}
|
||||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
return browserTimeZone;
|
||||||
});
|
});
|
||||||
|
|
||||||
const tzOffset = (date: Date | null): number => {
|
const tzOffset = computed((): number => {
|
||||||
if (timezone.value && date) {
|
if (!timezone.value) {
|
||||||
const eventUTCOffset = getTimezoneOffset(timezone.value, date);
|
return 0;
|
||||||
const localUTCOffset = getTimezoneOffset(userActualTimezone.value, date);
|
|
||||||
return (eventUTCOffset - localUTCOffset) / (60 * 1000);
|
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
};
|
const date = new Date();
|
||||||
|
|
||||||
|
// diff between UTC and selected timezone
|
||||||
|
// example: Asia/Shanghai is + 8 hours
|
||||||
|
const eventUTCOffset = getTimezoneOffset(timezone.value, date);
|
||||||
|
|
||||||
|
// diff between UTC and local browser timezone
|
||||||
|
// example: Europe/Paris is + 1 hour (or +2 depending of daylight saving time)
|
||||||
|
const localUTCOffset = getTimezoneOffset(browserTimeZone, date);
|
||||||
|
|
||||||
|
// example : offset is 8-1=7
|
||||||
|
return (eventUTCOffset - localUTCOffset) / (60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(tzOffset, () => {
|
||||||
|
// tzOffset has been changed, we need to update the event dates
|
||||||
|
updateEventDateRelatedToTimezone();
|
||||||
|
});
|
||||||
|
|
||||||
const eventPhysicalAddress = computed({
|
const eventPhysicalAddress = computed({
|
||||||
get(): IAddress | null {
|
get(): IAddress | null {
|
||||||
|
@ -1356,16 +1434,24 @@ const maximumAttendeeCapacity = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
|
||||||
|
|
||||||
const firstDayOfWeek = computed((): number => {
|
|
||||||
return dateFnsLocale?.options?.weekStartsOn || 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { event: fetchedEvent, onResult: onFetchEventResult } = useFetchEvent(
|
const { event: fetchedEvent, onResult: onFetchEventResult } = useFetchEvent(
|
||||||
eventId.value
|
eventId.value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// update the date components if the event changed (after fetching it, for example)
|
||||||
|
watch(event, () => {
|
||||||
|
if (event.value.beginsOn) {
|
||||||
|
const date = new Date(event.value.beginsOn);
|
||||||
|
date.setUTCMinutes(date.getUTCMinutes() + tzOffset.value);
|
||||||
|
beginsOn.value = date;
|
||||||
|
}
|
||||||
|
if (event.value.endsOn) {
|
||||||
|
const date = new Date(event.value.endsOn);
|
||||||
|
date.setUTCMinutes(date.getUTCMinutes() + tzOffset.value);
|
||||||
|
endsOn.value = date;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
fetchedEvent,
|
fetchedEvent,
|
||||||
() => {
|
() => {
|
||||||
|
@ -1434,4 +1520,27 @@ const registerOption = computed({
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#status .o-field--addons {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status .o-field--addons > label {
|
||||||
|
flex: 1 1 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#status .o-field--addons .mr-2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status .o-field--addons > label .o-radio__label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
#status .o-field--addons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
>
|
>
|
||||||
<start-time-icon
|
<start-time-icon
|
||||||
:date="event.beginsOn.toString()"
|
:date="event.beginsOn.toString()"
|
||||||
|
:timezone="event.options.timezone ?? undefined"
|
||||||
class="absolute right-3 -top-16"
|
class="absolute right-3 -top-16"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
>{{ t("Create event") }}</o-button
|
>{{ t("Create event") }}</o-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
|
|
||||||
<div class="flex flex-wrap gap-4 items-start">
|
<div class="flex flex-wrap gap-4 items-start">
|
||||||
<div
|
<div
|
||||||
class="rounded p-3 flex-auto md:flex-none bg-zinc-300 dark:bg-zinc-700"
|
class="rounded p-3 flex-auto md:flex-none bg-zinc-300 dark:bg-zinc-700"
|
||||||
|
@ -33,11 +32,11 @@
|
||||||
"
|
"
|
||||||
labelFor="events-start-datepicker"
|
labelFor="events-start-datepicker"
|
||||||
>
|
>
|
||||||
<o-datepicker
|
<event-date-picker
|
||||||
v-model="datePick"
|
|
||||||
:first-day-of-week="firstDayOfWeek"
|
|
||||||
id="events-start-datepicker"
|
id="events-start-datepicker"
|
||||||
/>
|
:time="false"
|
||||||
|
v-model="datePick"
|
||||||
|
></event-date-picker>
|
||||||
<o-button
|
<o-button
|
||||||
@click="datePick = new Date()"
|
@click="datePick = new Date()"
|
||||||
class="reset-area !h-auto"
|
class="reset-area !h-auto"
|
||||||
|
@ -137,13 +136,18 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-if="loading">
|
||||||
|
<div class="text-center prose dark:prose-invert max-w-full">
|
||||||
|
<p>{{ t("Loading…") }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section
|
<section
|
||||||
class="text-center not-found"
|
class="text-center not-found"
|
||||||
v-if="
|
v-if="
|
||||||
showUpcoming &&
|
showUpcoming &&
|
||||||
monthlyFutureEvents &&
|
monthlyFutureEvents &&
|
||||||
monthlyFutureEvents.size === 0 &&
|
monthlyFutureEvents.size === 0 &&
|
||||||
true // !$apollo.loading
|
!loading
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="text-center prose dark:prose-invert max-w-full">
|
<div class="text-center prose dark:prose-invert max-w-full">
|
||||||
|
@ -221,17 +225,17 @@ import {
|
||||||
LOGGED_USER_UPCOMING_EVENTS,
|
LOGGED_USER_UPCOMING_EVENTS,
|
||||||
} from "@/graphql/participant";
|
} from "@/graphql/participant";
|
||||||
import { useApolloClient, useQuery } from "@vue/apollo-composable";
|
import { useApolloClient, useQuery } from "@vue/apollo-composable";
|
||||||
import { computed, inject, ref, defineAsyncComponent } from "vue";
|
import { computed, ref, defineAsyncComponent } from "vue";
|
||||||
import { IUser } from "@/types/current-user.model";
|
import { IUser } from "@/types/current-user.model";
|
||||||
import {
|
import {
|
||||||
booleanTransformer,
|
booleanTransformer,
|
||||||
integerTransformer,
|
integerTransformer,
|
||||||
useRouteQuery,
|
useRouteQuery,
|
||||||
} from "vue-use-route-query";
|
} from "vue-use-route-query";
|
||||||
import { Locale } from "date-fns";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRestrictions } from "@/composition/apollo/config";
|
import { useRestrictions } from "@/composition/apollo/config";
|
||||||
import { useHead } from "@/utils/head";
|
import { useHead } from "@/utils/head";
|
||||||
|
import EventDatePicker from "@/components/Event/EventDatePicker.vue";
|
||||||
|
|
||||||
const EventParticipationCard = defineAsyncComponent(
|
const EventParticipationCard = defineAsyncComponent(
|
||||||
() => import("@/components/Event/EventParticipationCard.vue")
|
() => import("@/components/Event/EventParticipationCard.vue")
|
||||||
|
@ -246,7 +250,7 @@ const pastPage = ref(1);
|
||||||
const limit = ref(10);
|
const limit = ref(10);
|
||||||
|
|
||||||
function startOfDay(d: Date): string {
|
function startOfDay(d: Date): string {
|
||||||
const pad = (n: int): string => {
|
const pad = (n: number): string => {
|
||||||
return (n > 9 ? "" : "0") + n.toString();
|
return (n > 9 ? "" : "0") + n.toString();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
@ -291,6 +295,7 @@ const hasMorePastParticipations = ref(true);
|
||||||
const {
|
const {
|
||||||
result: loggedUserUpcomingEventsResult,
|
result: loggedUserUpcomingEventsResult,
|
||||||
fetchMore: fetchMoreUpcomingEvents,
|
fetchMore: fetchMoreUpcomingEvents,
|
||||||
|
loading,
|
||||||
} = useQuery<{
|
} = useQuery<{
|
||||||
loggedUser: IUser;
|
loggedUser: IUser;
|
||||||
}>(LOGGED_USER_UPCOMING_EVENTS, () => ({
|
}>(LOGGED_USER_UPCOMING_EVENTS, () => ({
|
||||||
|
@ -376,7 +381,7 @@ const monthlyFutureEvents = computed((): Map<string, Eventable[]> => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const monthlyPastParticipations = computed((): Map<string, Eventable[]> => {
|
const monthlyPastParticipations = computed((): Map<string, Eventable[]> => {
|
||||||
return monthlyEvents(pastParticipations.value.elements, true);
|
return monthlyEvents(pastParticipations.value.elements);
|
||||||
});
|
});
|
||||||
|
|
||||||
const monthParticipationsIds = (elements: Eventable[]): string[] => {
|
const monthParticipationsIds = (elements: Eventable[]): string[] => {
|
||||||
|
@ -490,12 +495,6 @@ const hideCreateEventButton = computed((): boolean => {
|
||||||
return restrictions.value?.onlyGroupsCanCreateEvents === true;
|
return restrictions.value?.onlyGroupsCanCreateEvents === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
|
||||||
|
|
||||||
const firstDayOfWeek = computed((): number => {
|
|
||||||
return dateFnsLocale?.options?.weekStartsOn ?? 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: computed(() => t("My events")),
|
title: computed(() => t("My events")),
|
||||||
});
|
});
|
||||||
|
|
|
@ -200,7 +200,13 @@
|
||||||
</o-checkbox>
|
</o-checkbox>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<o-button variant="primary" native-type="submit" class="mt-3">
|
<o-button
|
||||||
|
variant="primary"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="loading"
|
||||||
|
native-type="submit"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
{{ t("Create my group") }}
|
{{ t("Create my group") }}
|
||||||
</o-button>
|
</o-button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -371,7 +377,7 @@ const preferredUsernameErrors = computed(() => {
|
||||||
return [message, type];
|
return [message, type];
|
||||||
});
|
});
|
||||||
|
|
||||||
const { onDone, onError, mutate } = useCreateGroup();
|
const { onDone, onError, mutate, loading } = useCreateGroup();
|
||||||
|
|
||||||
onDone(() => {
|
onDone(() => {
|
||||||
notifier?.success(
|
notifier?.success(
|
||||||
|
|
|
@ -166,9 +166,12 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 my-2">
|
<div class="flex flex-wrap gap-2 my-2">
|
||||||
<o-button native-type="submit" variant="primary">{{
|
<o-button
|
||||||
t("Update group")
|
:loading="loadingUpdateGroup"
|
||||||
}}</o-button>
|
native-type="submit"
|
||||||
|
variant="primary"
|
||||||
|
>{{ t("Update group") }}</o-button
|
||||||
|
>
|
||||||
<o-button @click="confirmDeleteGroup" variant="danger">{{
|
<o-button @click="confirmDeleteGroup" variant="danger">{{
|
||||||
t("Delete group")
|
t("Delete group")
|
||||||
}}</o-button>
|
}}</o-button>
|
||||||
|
@ -244,7 +247,12 @@ const showCopiedTooltip = ref(false);
|
||||||
|
|
||||||
const editableGroup = ref<IGroup>();
|
const editableGroup = ref<IGroup>();
|
||||||
|
|
||||||
const { onDone, onError, mutate: updateGroup } = useUpdateGroup();
|
const {
|
||||||
|
onDone,
|
||||||
|
onError,
|
||||||
|
mutate: updateGroup,
|
||||||
|
loading: loadingUpdateGroup,
|
||||||
|
} = useUpdateGroup();
|
||||||
|
|
||||||
onDone(() => {
|
onDone(() => {
|
||||||
notifier?.success(t("Group settings saved"));
|
notifier?.success(t("Group settings saved"));
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto is-widescreen">
|
<div class="container mx-auto is-widescreen">
|
||||||
<div class="header flex flex-col">
|
<o-notification v-if="groupLoading" variant="info">
|
||||||
|
{{ t("Loading…") }}
|
||||||
|
</o-notification>
|
||||||
|
<o-notification v-if="!group && groupLoading === false" variant="danger">
|
||||||
|
{{ t("No group found") }}
|
||||||
|
</o-notification>
|
||||||
|
<div class="header flex flex-col" v-if="group">
|
||||||
<breadcrumbs-nav
|
<breadcrumbs-nav
|
||||||
v-if="group"
|
|
||||||
:links="[
|
:links="[
|
||||||
{ name: RouteName.MY_GROUPS, text: t('My groups') },
|
{ name: RouteName.MY_GROUPS, text: t('My groups') },
|
||||||
{
|
{
|
||||||
|
@ -12,8 +17,7 @@
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
|
<header class="block-container presentation">
|
||||||
<header class="block-container presentation" v-if="group">
|
|
||||||
<div class="banner-container">
|
<div class="banner-container">
|
||||||
<lazy-image-wrapper :picture="group.banner" />
|
<lazy-image-wrapper :picture="group.banner" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,64 +36,14 @@
|
||||||
<AccountGroup v-else :size="128" />
|
<AccountGroup v-else :size="128" />
|
||||||
</div>
|
</div>
|
||||||
<div class="title-container flex flex-1 flex-col text-center">
|
<div class="title-container flex flex-1 flex-col text-center">
|
||||||
<h1 class="m-0" v-if="group.name">
|
<h1 class="m-1" v-if="group.name">
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<!-- <o-skeleton v-else :animated="true" /> -->
|
<span dir="ltr" class="m-1" v-if="group.preferredUsername"
|
||||||
<span dir="ltr" class="" v-if="group.preferredUsername"
|
|
||||||
>@{{ usernameWithDomain(group) }}</span
|
>@{{ usernameWithDomain(group) }}</span
|
||||||
>
|
>
|
||||||
<!-- <o-skeleton v-else :animated="true" /> -->
|
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap justify-center flex-col md:flex-row">
|
<div class="flex flex-wrap justify-center flex-col md:flex-row">
|
||||||
<div
|
|
||||||
class="flex flex-col items-center flex-1 m-0"
|
|
||||||
v-if="isCurrentActorAGroupMember && !previewPublic && members"
|
|
||||||
>
|
|
||||||
<div class="flex">
|
|
||||||
<figure
|
|
||||||
:title="
|
|
||||||
t(`{'@'}{username} ({role})`, {
|
|
||||||
username: usernameWithDomain(member.actor),
|
|
||||||
role: member.role,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
v-for="member in members.elements"
|
|
||||||
:key="member.actor.id"
|
|
||||||
class="-mr-3"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="rounded-full h-8"
|
|
||||||
:src="member.actor.avatar.url"
|
|
||||||
v-if="member.actor.avatar"
|
|
||||||
alt=""
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
/>
|
|
||||||
<AccountCircle v-else :size="32" />
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
t(
|
|
||||||
"{count} members",
|
|
||||||
{
|
|
||||||
count: group.members?.total,
|
|
||||||
},
|
|
||||||
group.members?.total
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
<router-link
|
|
||||||
v-if="isCurrentActorAGroupAdmin"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.GROUP_MEMBERS_SETTINGS,
|
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
|
||||||
}"
|
|
||||||
>{{ t("Add / Remove…") }}</router-link
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-3 justify-center">
|
<div class="flex flex-wrap gap-3 justify-center">
|
||||||
<o-button
|
<o-button
|
||||||
outlined
|
outlined
|
||||||
|
@ -376,12 +330,14 @@
|
||||||
:invitations="[groupMember]"
|
:invitations="[groupMember]"
|
||||||
/>
|
/>
|
||||||
<o-notification
|
<o-notification
|
||||||
|
class="my-2"
|
||||||
v-if="isCurrentActorARejectedGroupMember"
|
v-if="isCurrentActorARejectedGroupMember"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
>
|
>
|
||||||
{{ t("You have been removed from this group's members.") }}
|
{{ t("You have been removed from this group's members.") }}
|
||||||
</o-notification>
|
</o-notification>
|
||||||
<o-notification
|
<o-notification
|
||||||
|
class="my-2"
|
||||||
v-if="
|
v-if="
|
||||||
isCurrentActorAGroupMember &&
|
isCurrentActorAGroupMember &&
|
||||||
isCurrentActorARecentMember &&
|
isCurrentActorARecentMember &&
|
||||||
|
@ -395,47 +351,11 @@
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</o-notification>
|
</o-notification>
|
||||||
</div>
|
<o-notification
|
||||||
</header>
|
class="my-2"
|
||||||
</div>
|
v-if="group && group.domain && !isCurrentActorAGroupMember"
|
||||||
<div
|
variant="info"
|
||||||
v-if="isCurrentActorAGroupMember && !previewPublic && group"
|
>
|
||||||
class="block-container flex gap-2 flex-wrap mt-3"
|
|
||||||
>
|
|
||||||
<!-- Private things -->
|
|
||||||
<div class="flex-1 m-0 flex flex-col flex-wrap gap-2">
|
|
||||||
<!-- Group discussions -->
|
|
||||||
<Discussions :group="discussionGroup ?? group" class="flex-1" />
|
|
||||||
<!-- Resources -->
|
|
||||||
<Resources :group="resourcesGroup ?? group" class="flex-1" />
|
|
||||||
</div>
|
|
||||||
<!-- Public things -->
|
|
||||||
<div class="flex-1 m-0 flex flex-col flex-wrap gap-2">
|
|
||||||
<!-- Events -->
|
|
||||||
<Events
|
|
||||||
:group="group"
|
|
||||||
:isModerator="isCurrentActorAGroupModerator"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<!-- Posts -->
|
|
||||||
<Posts
|
|
||||||
:group="group"
|
|
||||||
:isModerator="isCurrentActorAGroupModerator"
|
|
||||||
:isMember="isCurrentActorAGroupMember"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<o-notification
|
|
||||||
v-else-if="!group && groupLoading === false"
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
{{ t("No group found") }}
|
|
||||||
</o-notification>
|
|
||||||
<div v-else-if="group" class="public-container flex flex-col">
|
|
||||||
<aside class="group-metadata">
|
|
||||||
<div class="sticky">
|
|
||||||
<o-notification v-if="group.domain && !isCurrentActorAGroupMember">
|
|
||||||
<p>
|
<p>
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
|
@ -451,49 +371,120 @@
|
||||||
>{{ t("View full profile") }}</o-button
|
>{{ t("View full profile") }}</o-button
|
||||||
>
|
>
|
||||||
</o-notification>
|
</o-notification>
|
||||||
<event-metadata-block
|
</div>
|
||||||
:title="t('About')"
|
</header>
|
||||||
v-if="group.summary && group.summary !== '<p></p>'"
|
</div>
|
||||||
>
|
<div v-if="group" class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2">
|
||||||
|
<!-- Public thing: Members -->
|
||||||
|
<group-section :title="t('Members')" icon="account-group">
|
||||||
|
<template #default>
|
||||||
|
<div class="flex flex-col justify-center h-full">
|
||||||
<div
|
<div
|
||||||
dir="auto"
|
class="flex flex-col items-center"
|
||||||
class="prose lg:prose-xl dark:prose-invert"
|
v-if="isCurrentActorAGroupMember && !previewPublic && members"
|
||||||
v-html="group.summary"
|
>
|
||||||
/>
|
<div class="flex">
|
||||||
</event-metadata-block>
|
<figure
|
||||||
<event-metadata-block :title="t('Members')">
|
:title="
|
||||||
<template #icon>
|
t(`{'@'}{username} ({role})`, {
|
||||||
<AccountGroup :size="48" />
|
username: usernameWithDomain(member.actor),
|
||||||
</template>
|
role: member.role,
|
||||||
{{
|
})
|
||||||
t(
|
"
|
||||||
"{count} members",
|
v-for="member in members.elements"
|
||||||
{
|
:key="member.actor.id"
|
||||||
count: group.members?.total,
|
class="-mr-3"
|
||||||
},
|
>
|
||||||
group.members?.total
|
<img
|
||||||
)
|
class="rounded-full h-8"
|
||||||
}}
|
:src="member.actor.avatar.url"
|
||||||
</event-metadata-block>
|
v-if="member.actor.avatar"
|
||||||
<event-metadata-block
|
alt=""
|
||||||
v-if="physicalAddress && physicalAddress.url"
|
width="32"
|
||||||
:title="t('Location')"
|
height="32"
|
||||||
|
/>
|
||||||
|
<AccountCircle v-else :size="32" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<h2 class="text-center">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"{count} members",
|
||||||
|
{
|
||||||
|
count: group.members?.total,
|
||||||
|
},
|
||||||
|
group.members?.total
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div></template
|
||||||
|
>
|
||||||
|
<template #create>
|
||||||
|
<o-button
|
||||||
|
v-if="isCurrentActorAGroupAdmin && !previewPublic"
|
||||||
|
tag="router-link"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.GROUP_MEMBERS_SETTINGS,
|
||||||
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
|
}"
|
||||||
|
class="button is-primary"
|
||||||
|
>{{ t("Add / Remove…") }}</o-button
|
||||||
>
|
>
|
||||||
<template #icon>
|
</template>
|
||||||
<o-icon
|
</group-section>
|
||||||
v-if="physicalAddress.poiInfos.poiIcon.icon"
|
<!-- Public thing: About -->
|
||||||
:icon="physicalAddress.poiInfos.poiIcon.icon"
|
<group-section :title="t('About')" icon="information">
|
||||||
customSize="48"
|
<template #default>
|
||||||
/>
|
<div
|
||||||
<Earth v-else :size="48" />
|
v-if="group.summary"
|
||||||
</template>
|
dir="auto"
|
||||||
|
class="prose lg:prose-xl dark:prose-invert p-2"
|
||||||
|
v-html="group.summary"
|
||||||
|
></div>
|
||||||
|
<empty-content
|
||||||
|
v-else
|
||||||
|
icon="information"
|
||||||
|
:inline="true"
|
||||||
|
:center="true"
|
||||||
|
>
|
||||||
|
{{ t("No about content yet") }}
|
||||||
|
</empty-content>
|
||||||
|
</template>
|
||||||
|
<template #create>
|
||||||
|
<o-button
|
||||||
|
v-if="isCurrentActorAGroupAdmin && !previewPublic"
|
||||||
|
tag="router-link"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.GROUP_PUBLIC_SETTINGS,
|
||||||
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
|
}"
|
||||||
|
class="button is-primary"
|
||||||
|
>{{ t("Edit") }}</o-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</group-section>
|
||||||
|
<!-- Public thing: Location -->
|
||||||
|
<group-section :title="t('Location')" icon="earth">
|
||||||
|
<template #default
|
||||||
|
><div
|
||||||
|
class="flex flex-col justify-center h-full"
|
||||||
|
v-if="physicalAddress && physicalAddress.url"
|
||||||
|
>
|
||||||
|
<o-icon
|
||||||
|
v-if="physicalAddress.poiInfos.poiIcon.icon"
|
||||||
|
:icon="physicalAddress.poiInfos.poiIcon.icon"
|
||||||
|
customSize="48"
|
||||||
|
/>
|
||||||
|
<Earth v-else :size="48" />
|
||||||
<div class="address-wrapper">
|
<div class="address-wrapper">
|
||||||
<span
|
<div class="address">
|
||||||
v-if="!physicalAddress || !addressFullName(physicalAddress)"
|
<div class="text-center">
|
||||||
>{{ t("No address defined") }}</span
|
<span v-if="!addressFullName(physicalAddress)">{{
|
||||||
>
|
t("No address defined")
|
||||||
<div class="address" v-if="physicalAddress">
|
}}</span>
|
||||||
<div>
|
|
||||||
<address dir="auto">
|
<address dir="auto">
|
||||||
<p
|
<p
|
||||||
class="addressDescription"
|
class="addressDescription"
|
||||||
|
@ -506,118 +497,90 @@
|
||||||
</p>
|
</p>
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
<o-button
|
|
||||||
class="map-show-button"
|
|
||||||
variant="text"
|
|
||||||
@click="showMap = !showMap"
|
|
||||||
@keyup.enter="showMap = !showMap"
|
|
||||||
v-if="physicalAddress.geom"
|
|
||||||
>
|
|
||||||
{{ t("Show map") }}
|
|
||||||
</o-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</event-metadata-block>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="main-content min-w-min flex-auto py-0 px-2">
|
|
||||||
<section>
|
|
||||||
<h2 class="text-2xl font-bold">{{ t("Upcoming events") }}</h2>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-3"
|
|
||||||
v-if="group && organizedEvents.elements.length > 0"
|
|
||||||
>
|
|
||||||
<event-minimalist-card
|
|
||||||
v-for="event in organizedEvents.elements.slice(0, 3)"
|
|
||||||
:event="event"
|
|
||||||
:key="event.uuid"
|
|
||||||
class="organized-event"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<empty-content
|
<empty-content v-else icon="earth" :inline="true" :center="true">
|
||||||
v-else-if="group"
|
{{ t("No location yet") }}
|
||||||
icon="calendar"
|
</empty-content></template
|
||||||
:inline="true"
|
>
|
||||||
description-classes="flex flex-col items-stretch"
|
<template #create>
|
||||||
>
|
|
||||||
{{ t("No public upcoming events") }}
|
|
||||||
<template #desc>
|
|
||||||
<template v-if="isCurrentActorFollowing">
|
|
||||||
<i18n-t
|
|
||||||
keypath="You will receive notifications about this group's public activity depending on %{notification_settings}."
|
|
||||||
>
|
|
||||||
<template #notification_settings>
|
|
||||||
<router-link :to="{ name: RouteName.NOTIFICATIONS }">{{
|
|
||||||
t("your notification settings")
|
|
||||||
}}</router-link>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</template>
|
|
||||||
<o-button
|
|
||||||
tag="router-link"
|
|
||||||
class="my-2 self-center"
|
|
||||||
variant="text"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.GROUP_EVENTS,
|
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
|
||||||
query: { showPassedEvents: true },
|
|
||||||
}"
|
|
||||||
>{{ t("View past events") }}</o-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</empty-content>
|
|
||||||
<!-- <o-skeleton animated v-else-if="$apollo.loading"></o-skeleton> -->
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<o-button
|
|
||||||
tag="router-link"
|
|
||||||
class="my-4"
|
|
||||||
variant="text"
|
|
||||||
v-if="organizedEvents.total > 0"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.GROUP_EVENTS,
|
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
|
||||||
query: {
|
|
||||||
showPassedEvents: organizedEvents.elements.length === 0,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>{{ t("View all events") }}</o-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="flex flex-col items-stretch">
|
|
||||||
<h2 class="ml-0 text-2xl font-bold">{{ t("Latest posts") }}</h2>
|
|
||||||
|
|
||||||
<multi-post-list-item
|
|
||||||
v-if="
|
|
||||||
posts.elements.filter(
|
|
||||||
(post) =>
|
|
||||||
!post.draft && post.visibility === PostVisibility.PUBLIC
|
|
||||||
).length > 0
|
|
||||||
"
|
|
||||||
:posts="
|
|
||||||
posts.elements.filter(
|
|
||||||
(post) =>
|
|
||||||
!post.draft && post.visibility === PostVisibility.PUBLIC
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<empty-content v-else-if="group" icon="bullhorn" :inline="true">
|
|
||||||
{{ t("No posts yet") }}
|
|
||||||
</empty-content>
|
|
||||||
<!-- <o-skeleton animated v-else-if="$apollo.loading"></o-skeleton> -->
|
|
||||||
<o-button
|
<o-button
|
||||||
class="self-center my-2"
|
v-if="physicalAddress && physicalAddress.geom"
|
||||||
v-if="posts.total > 0"
|
|
||||||
tag="router-link"
|
|
||||||
variant="text"
|
variant="text"
|
||||||
|
@click="showMap = !showMap"
|
||||||
|
@keyup.enter="showMap = !showMap"
|
||||||
|
>
|
||||||
|
{{ t("Show map") }}
|
||||||
|
</o-button>
|
||||||
|
<o-button
|
||||||
|
v-if="isCurrentActorAGroupAdmin && !previewPublic"
|
||||||
|
tag="router-link"
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.POSTS,
|
name: RouteName.GROUP_PUBLIC_SETTINGS,
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
}"
|
}"
|
||||||
>{{ t("View all posts") }}</o-button
|
class="button is-primary"
|
||||||
|
>{{ t("Edit") }}</o-button
|
||||||
>
|
>
|
||||||
</section>
|
</template>
|
||||||
|
</group-section>
|
||||||
|
</div>
|
||||||
|
<div v-if="group">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'grid grid-cols-1 gap-2 mb-2',
|
||||||
|
{ 'xl:grid-cols-3': isLongEvents, 'md:grid-cols-2': !isLongEvents },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- Public thing: Long Events -->
|
||||||
|
<Events
|
||||||
|
v-if="isLongEvents"
|
||||||
|
:group="group"
|
||||||
|
:isModerator="isCurrentActorAGroupModerator && !previewPublic"
|
||||||
|
:longEvent="true"
|
||||||
|
/>
|
||||||
|
<!-- Public thing: Events -->
|
||||||
|
<Events
|
||||||
|
:group="group"
|
||||||
|
:isModerator="isCurrentActorAGroupModerator && !previewPublic"
|
||||||
|
:longEvent="false"
|
||||||
|
/>
|
||||||
|
<!-- Public thing: Posts -->
|
||||||
|
<Posts
|
||||||
|
:group="group"
|
||||||
|
:isModerator="isCurrentActorAGroupModerator && !previewPublic"
|
||||||
|
:isMember="isCurrentActorAGroupMember && !previewPublic"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-2 mb-2 md:grid-cols-2">
|
||||||
|
<!-- Private thing: Group discussions -->
|
||||||
|
<Discussions
|
||||||
|
v-if="isCurrentActorAGroupMember && !previewPublic"
|
||||||
|
:group="discussionGroup ?? group"
|
||||||
|
/>
|
||||||
|
<!-- Private thing: Resources -->
|
||||||
|
<Resources
|
||||||
|
v-if="isCurrentActorAGroupMember && !previewPublic"
|
||||||
|
:group="resourcesGroup ?? group"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-2">
|
||||||
|
<template v-if="isCurrentActorFollowing">
|
||||||
|
<i18n-t
|
||||||
|
class="my-2"
|
||||||
|
keypath="You will receive notifications about this group's public activity depending on %{notification_settings}."
|
||||||
|
>
|
||||||
|
<template #notification_settings>
|
||||||
|
<router-link :to="{ name: RouteName.NOTIFICATIONS }">{{
|
||||||
|
t("your notification settings")
|
||||||
|
}}</router-link>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="group" class="public-container flex flex-col">
|
||||||
<o-modal
|
<o-modal
|
||||||
v-if="physicalAddress && physicalAddress.geom"
|
v-if="physicalAddress && physicalAddress.geom"
|
||||||
v-model:active="showMap"
|
v-model:active="showMap"
|
||||||
|
@ -655,7 +618,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// import EventCard from "@/components/Event/EventCard.vue";
|
|
||||||
import {
|
import {
|
||||||
displayName,
|
displayName,
|
||||||
IActor,
|
IActor,
|
||||||
|
@ -663,14 +625,11 @@ import {
|
||||||
IPerson,
|
IPerson,
|
||||||
usernameWithDomain,
|
usernameWithDomain,
|
||||||
} from "@/types/actor";
|
} from "@/types/actor";
|
||||||
// import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
|
||||||
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
|
|
||||||
import MultiPostListItem from "@/components/Post/MultiPostListItem.vue";
|
|
||||||
import { Address, addressFullName } from "@/types/address.model";
|
import { Address, addressFullName } from "@/types/address.model";
|
||||||
import InvitationsList from "@/components/Group/InvitationsList.vue";
|
import InvitationsList from "@/components/Group/InvitationsList.vue";
|
||||||
import { addMinutes } from "date-fns";
|
import { addMinutes } from "date-fns";
|
||||||
import { JOIN_GROUP } from "@/graphql/member";
|
import { JOIN_GROUP } from "@/graphql/member";
|
||||||
import { MemberRole, Openness, PostVisibility } from "@/types/enums";
|
import { MemberRole, Openness } from "@/types/enums";
|
||||||
import { IMember } from "@/types/actor/member.model";
|
import { IMember } from "@/types/actor/member.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import ReportModal from "@/components/Report/ReportModal.vue";
|
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||||
|
@ -679,11 +638,7 @@ import {
|
||||||
PERSON_STATUS_GROUP,
|
PERSON_STATUS_GROUP,
|
||||||
} from "@/graphql/actor";
|
} from "@/graphql/actor";
|
||||||
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
|
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
|
||||||
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
|
|
||||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
import { IEvent } from "@/types/event.model";
|
|
||||||
import { IPost } from "@/types/post.model";
|
|
||||||
import {
|
import {
|
||||||
FOLLOW_GROUP,
|
FOLLOW_GROUP,
|
||||||
UNFOLLOW_GROUP,
|
UNFOLLOW_GROUP,
|
||||||
|
@ -716,6 +671,8 @@ import { Dialog } from "@/plugins/dialog";
|
||||||
import { Notifier } from "@/plugins/notifier";
|
import { Notifier } from "@/plugins/notifier";
|
||||||
import { useGroupResourcesList } from "@/composition/apollo/resources";
|
import { useGroupResourcesList } from "@/composition/apollo/resources";
|
||||||
import { useGroupMembers } from "@/composition/apollo/members";
|
import { useGroupMembers } from "@/composition/apollo/members";
|
||||||
|
import GroupSection from "@/components/Group/GroupSection.vue";
|
||||||
|
import { useIsLongEvents } from "@/composition/apollo/config";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
preferredUsername: string;
|
preferredUsername: string;
|
||||||
|
@ -740,6 +697,8 @@ const { group: resourcesGroup } = useGroupResourcesList(preferredUsername, {
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
const { isLongEvents } = useIsLongEvents();
|
||||||
|
|
||||||
// const { person } = usePersonStatusGroup(group);
|
// const { person } = usePersonStatusGroup(group);
|
||||||
|
|
||||||
const { result, subscribeToMore } = useQuery<{
|
const { result, subscribeToMore } = useQuery<{
|
||||||
|
@ -1093,32 +1052,6 @@ const ableToReport = computed((): boolean => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const organizedEvents = computed((): Paginate<IEvent> => {
|
|
||||||
return {
|
|
||||||
total: group.value?.organizedEvents.total ?? 0,
|
|
||||||
elements:
|
|
||||||
group.value?.organizedEvents.elements.filter((event: IEvent) => {
|
|
||||||
if (previewPublic.value) {
|
|
||||||
return !event.draft; // TODO when events get visibility access add visibility constraint like below for posts
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}) ?? [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const posts = computed((): Paginate<IPost> => {
|
|
||||||
return {
|
|
||||||
total: group.value?.posts.total ?? 0,
|
|
||||||
elements:
|
|
||||||
group.value?.posts.elements.filter((post: IPost) => {
|
|
||||||
if (previewPublic.value || !isCurrentActorAGroupMember.value) {
|
|
||||||
return !post.draft && post.visibility == PostVisibility.PUBLIC;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}) ?? [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const showFollowButton = computed((): boolean => {
|
const showFollowButton = computed((): boolean => {
|
||||||
return !isCurrentActorFollowing.value || previewPublic.value;
|
return !isCurrentActorFollowing.value || previewPublic.value;
|
||||||
});
|
});
|
||||||
|
@ -1255,10 +1188,6 @@ div.container {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.map-show-button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
address {
|
address {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
<!-- Search fields -->
|
<!-- Search fields -->
|
||||||
<search-fields
|
<search-fields
|
||||||
v-model:search="search"
|
v-model:search="search"
|
||||||
v-model:location="location"
|
v-model:address="userAddress"
|
||||||
:locationDefaultText="location?.description"
|
v-model:distance="distance"
|
||||||
|
v-on:update:address="updateAddress"
|
||||||
:fromLocalStorage="true"
|
:fromLocalStorage="true"
|
||||||
|
:addressDefaultText="userLocation?.name"
|
||||||
|
:key="increated"
|
||||||
/>
|
/>
|
||||||
<!-- Categories preview
|
|
||||||
<categories-preview /> -->
|
|
||||||
<!-- Welcome back -->
|
<!-- Welcome back -->
|
||||||
<section
|
<section
|
||||||
class="container mx-auto"
|
class="container mx-auto"
|
||||||
|
@ -121,6 +122,7 @@
|
||||||
@doGeoLoc="performGeoLocation()"
|
@doGeoLoc="performGeoLocation()"
|
||||||
:userLocation="userLocation"
|
:userLocation="userLocation"
|
||||||
:doingGeoloc="doingGeoloc"
|
:doingGeoloc="doingGeoloc"
|
||||||
|
:distance="distance"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -154,11 +156,16 @@ import {
|
||||||
UPDATE_CURRENT_USER_LOCATION_CLIENT,
|
UPDATE_CURRENT_USER_LOCATION_CLIENT,
|
||||||
} from "@/graphql/location";
|
} from "@/graphql/location";
|
||||||
import { LocationType } from "@/types/user-location.model";
|
import { LocationType } from "@/types/user-location.model";
|
||||||
import CategoriesPreview from "@/components/Home/CategoriesPreview.vue";
|
|
||||||
import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
|
import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
|
||||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||||
import { useHead } from "@unhead/vue";
|
import { useHead } from "@unhead/vue";
|
||||||
import { addressToLocation, geoHashToCoords } from "@/utils/location";
|
import {
|
||||||
|
addressToLocation,
|
||||||
|
geoHashToCoords,
|
||||||
|
getAddressFromLocal,
|
||||||
|
locationToAddress,
|
||||||
|
storeAddressInLocal,
|
||||||
|
} from "@/utils/location";
|
||||||
import { useServerProvidedLocation } from "@/composition/apollo/config";
|
import { useServerProvidedLocation } from "@/composition/apollo/config";
|
||||||
import { ABOUT } from "@/graphql/config";
|
import { ABOUT } from "@/graphql/config";
|
||||||
import { IConfig } from "@/types/config.model";
|
import { IConfig } from "@/types/config.model";
|
||||||
|
@ -211,11 +218,14 @@ const currentUserParticipations = computed(
|
||||||
() => loggedUser.value?.participations.elements
|
() => loggedUser.value?.participations.elements
|
||||||
);
|
);
|
||||||
|
|
||||||
const location = ref(null);
|
const increated = ref(0);
|
||||||
const search = ref("");
|
const address = ref(null);
|
||||||
|
const search = ref(null);
|
||||||
|
const noAddress = ref(false);
|
||||||
|
const current_distance = ref(null);
|
||||||
|
|
||||||
watch(location, (newLoc, oldLoc) =>
|
watch(address, (newAdd, oldAdd) =>
|
||||||
console.debug("LOCATION UPDATED from", { ...oldLoc }, " to ", { ...newLoc })
|
console.debug("ADDRESS UPDATED from", { ...oldAdd }, " to ", { ...newAdd })
|
||||||
);
|
);
|
||||||
|
|
||||||
const isToday = (date: string): boolean => {
|
const isToday = (date: string): boolean => {
|
||||||
|
@ -372,15 +382,25 @@ const { result: reverseGeocodeResult } = useQuery<{
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const userSettingsLocation = computed(() => {
|
const userSettingsLocation = computed(() => {
|
||||||
const address = reverseGeocodeResult.value?.reverseGeocode[0];
|
const location = reverseGeocodeResult.value?.reverseGeocode[0];
|
||||||
const placeName = address?.locality ?? address?.region ?? address?.country;
|
const placeName = location?.locality ?? location?.region ?? location?.country;
|
||||||
return {
|
console.debug(
|
||||||
lat: coords.value?.latitude,
|
"userSettingsLocation from reverseGeocode",
|
||||||
lon: coords.value?.longitude,
|
reverseGeocodeResult.value,
|
||||||
name: placeName,
|
coords.value,
|
||||||
picture: address?.pictureInfo,
|
placeName
|
||||||
isIPLocation: coords.value?.isIPLocation,
|
);
|
||||||
};
|
if (placeName) {
|
||||||
|
return {
|
||||||
|
lat: coords.value?.latitude,
|
||||||
|
lon: coords.value?.longitude,
|
||||||
|
name: placeName,
|
||||||
|
picture: location?.pictureInfo,
|
||||||
|
isIPLocation: coords.value?.isIPLocation,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { result: currentUserLocationResult } = useQuery<{
|
const { result: currentUserLocationResult } = useQuery<{
|
||||||
|
@ -389,6 +409,10 @@ const { result: currentUserLocationResult } = useQuery<{
|
||||||
|
|
||||||
// The user's location currently in the Apollo cache
|
// The user's location currently in the Apollo cache
|
||||||
const currentUserLocation = computed(() => {
|
const currentUserLocation = computed(() => {
|
||||||
|
console.debug(
|
||||||
|
"currentUserLocation from LocationType",
|
||||||
|
currentUserLocationResult.value
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...(currentUserLocationResult.value?.currentUserLocation ?? {
|
...(currentUserLocationResult.value?.currentUserLocation ?? {
|
||||||
lat: undefined,
|
lat: undefined,
|
||||||
|
@ -404,9 +428,20 @@ const currentUserLocation = computed(() => {
|
||||||
|
|
||||||
const userLocation = computed(() => {
|
const userLocation = computed(() => {
|
||||||
console.debug("new userLocation");
|
console.debug("new userLocation");
|
||||||
if (location.value) {
|
if (noAddress.value) {
|
||||||
|
return {
|
||||||
|
lon: null,
|
||||||
|
lat: null,
|
||||||
|
name: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (address.value) {
|
||||||
console.debug("userLocation is typed location");
|
console.debug("userLocation is typed location");
|
||||||
return addressToLocation(location.value);
|
return addressToLocation(address.value);
|
||||||
|
}
|
||||||
|
const local_address = getAddressFromLocal();
|
||||||
|
if (local_address) {
|
||||||
|
return addressToLocation(local_address);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!userSettingsLocation.value ||
|
!userSettingsLocation.value ||
|
||||||
|
@ -418,6 +453,47 @@ const userLocation = computed(() => {
|
||||||
return userSettingsLocation.value;
|
return userSettingsLocation.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userAddress = computed({
|
||||||
|
get(): IAddress | null {
|
||||||
|
if (noAddress.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (address.value) {
|
||||||
|
return address.value;
|
||||||
|
}
|
||||||
|
const local_address = getAddressFromLocal();
|
||||||
|
if (local_address) {
|
||||||
|
return local_address;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!userSettingsLocation.value ||
|
||||||
|
(userSettingsLocation.value?.isIPLocation &&
|
||||||
|
currentUserLocation.value?.name)
|
||||||
|
) {
|
||||||
|
return locationToAddress(currentUserLocation.value);
|
||||||
|
}
|
||||||
|
return locationToAddress(userSettingsLocation.value);
|
||||||
|
},
|
||||||
|
set(newAddress: IAddress | null) {
|
||||||
|
address.value = newAddress;
|
||||||
|
noAddress.value = newAddress == null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const distance = computed({
|
||||||
|
get(): number | null {
|
||||||
|
if (noAddress.value || !userLocation.value?.name) {
|
||||||
|
return null;
|
||||||
|
} else if (current_distance.value == null) {
|
||||||
|
return userLocation.value?.isIPLocation ? 150 : 25;
|
||||||
|
}
|
||||||
|
return current_distance.value;
|
||||||
|
},
|
||||||
|
set(newDistance: number) {
|
||||||
|
current_distance.value = newDistance;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutate: saveCurrentUserLocation } = useMutation<any, LocationType>(
|
const { mutate: saveCurrentUserLocation } = useMutation<any, LocationType>(
|
||||||
UPDATE_CURRENT_USER_LOCATION_CLIENT
|
UPDATE_CURRENT_USER_LOCATION_CLIENT
|
||||||
);
|
);
|
||||||
|
@ -478,6 +554,15 @@ const performGeoLocation = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateAddress = (newAddress: IAddress | null) => {
|
||||||
|
if (address.value?.geom != newAddress?.geom || newAddress == null) {
|
||||||
|
increated.value += 1;
|
||||||
|
storeAddressInLocal(newAddress);
|
||||||
|
}
|
||||||
|
address.value = newAddress;
|
||||||
|
noAddress.value = newAddress == null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View Head
|
* View Head
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
<search-fields
|
<search-fields
|
||||||
class="md:ml-10 mr-2"
|
class="md:ml-10 mr-2"
|
||||||
v-model:search="search"
|
v-model:search="search"
|
||||||
v-model:location="location"
|
v-model:address="address"
|
||||||
:locationDefaultText="locationName"
|
v-model:distance="radius"
|
||||||
|
:addressDefaultText="addressName"
|
||||||
:fromLocalStorage="true"
|
:fromLocalStorage="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,51 +28,6 @@
|
||||||
:class="{ hidden: filtersPanelOpened }"
|
:class="{ hidden: filtersPanelOpened }"
|
||||||
class="lg:block mt-4 px-2"
|
class="lg:block mt-4 px-2"
|
||||||
>
|
>
|
||||||
<p class="sr-only">{{ t("Type") }}</p>
|
|
||||||
<ul
|
|
||||||
class="font-medium text-gray-900 dark:text-slate-100 space-y-4 pb-4 border-b border-gray-200 dark:border-gray-500"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="content in contentTypeMapping"
|
|
||||||
:key="content.contentType"
|
|
||||||
class="flex gap-1"
|
|
||||||
>
|
|
||||||
<Magnify
|
|
||||||
v-if="content.contentType === ContentType.ALL"
|
|
||||||
:size="24"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Calendar
|
|
||||||
v-if="content.contentType === ContentType.EVENTS"
|
|
||||||
:size="24"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Calendar
|
|
||||||
v-if="content.contentType === ContentType.SHORTEVENTS"
|
|
||||||
:size="24"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarStar
|
|
||||||
v-if="content.contentType === ContentType.LONGEVENTS"
|
|
||||||
:size="24"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AccountMultiple
|
|
||||||
v-if="content.contentType === ContentType.GROUPS"
|
|
||||||
:size="24"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
...$route,
|
|
||||||
query: { ...$route.query, contentType: content.contentType },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ content.label }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="py-4 border-b border-gray-200 dark:border-gray-500"
|
class="py-4 border-b border-gray-200 dark:border-gray-500"
|
||||||
v-show="globalSearchEnabled"
|
v-show="globalSearchEnabled"
|
||||||
|
@ -90,7 +46,7 @@
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
for="selfTarget"
|
for="selfTarget"
|
||||||
class="ml-3 font-medium text-gray-900 dark:text-gray-300"
|
class="cursor-pointer ml-3 font-medium text-gray-900 dark:text-gray-300"
|
||||||
>{{ t("From this instance only") }}</label
|
>{{ t("From this instance only") }}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,25 +62,10 @@
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
for="internalTarget"
|
for="internalTarget"
|
||||||
class="ml-3 font-medium text-gray-900 dark:text-gray-300"
|
class="cursor-pointer ml-3 font-medium text-gray-900 dark:text-gray-300"
|
||||||
>{{ t("In this instance's network") }}</label
|
>{{ t("In this instance's network") }}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
id="globalTarget"
|
|
||||||
v-model="searchTarget"
|
|
||||||
type="radio"
|
|
||||||
name="searchTarget"
|
|
||||||
:value="SearchTargets.GLOBAL"
|
|
||||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="globalTarget"
|
|
||||||
class="ml-3 font-medium text-gray-900 dark:text-gray-300"
|
|
||||||
>{{ t("On the Fediverse") }}</label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -157,7 +98,7 @@
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
:for="key"
|
:for="key"
|
||||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||||
>{{ eventStartDateRangeOption.label }}</label
|
>{{ eventStartDateRangeOption.label }}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -175,43 +116,6 @@
|
||||||
</template>
|
</template>
|
||||||
</filter-section>
|
</filter-section>
|
||||||
|
|
||||||
<filter-section
|
|
||||||
v-show="!isOnline"
|
|
||||||
v-model:opened="searchFilterSectionsOpenStatus.eventDistance"
|
|
||||||
:title="t('Distance')"
|
|
||||||
>
|
|
||||||
<template #options>
|
|
||||||
<fieldset class="flex flex-col">
|
|
||||||
<legend class="sr-only">{{ t("Distance") }}</legend>
|
|
||||||
<div
|
|
||||||
v-for="distanceOption in eventDistance"
|
|
||||||
:key="distanceOption.id"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:id="distanceOption.id"
|
|
||||||
v-model="distance"
|
|
||||||
type="radio"
|
|
||||||
name="eventDistance"
|
|
||||||
:value="distanceOption.id"
|
|
||||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
:for="distanceOption.id"
|
|
||||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
|
||||||
>{{ distanceOption.label }}</label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</template>
|
|
||||||
<template #preview>
|
|
||||||
<span
|
|
||||||
class="bg-blue-100 text-blue-800 text-sm font-semibold p-0.5 rounded dark:bg-blue-200 dark:text-blue-800 grow-0"
|
|
||||||
>
|
|
||||||
{{ eventDistance.find(({ id }) => id === distance)?.label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</filter-section>
|
|
||||||
|
|
||||||
<filter-section
|
<filter-section
|
||||||
v-show="contentType !== 'GROUPS'"
|
v-show="contentType !== 'GROUPS'"
|
||||||
v-model:opened="searchFilterSectionsOpenStatus.eventCategory"
|
v-model:opened="searchFilterSectionsOpenStatus.eventCategory"
|
||||||
|
@ -231,7 +135,7 @@
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
:for="category.id"
|
:for="category.id"
|
||||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||||
>{{ category.label }}</label
|
>{{ category.label }}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -292,7 +196,7 @@
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
:for="eventStatusOption.id"
|
:for="eventStatusOption.id"
|
||||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||||
>{{ eventStatusOption.label }}</label
|
>{{ eventStatusOption.label }}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -339,7 +243,7 @@
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
:for="key"
|
:for="key"
|
||||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||||
>{{ language }}</label
|
>{{ language }}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -375,61 +279,6 @@
|
||||||
</template>
|
</template>
|
||||||
</filter-section>
|
</filter-section>
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
<div class="">
|
|
||||||
<label v-translate class="font-bold" for="host">Mobilizon instance</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="host"
|
|
||||||
v-model="formHost"
|
|
||||||
type="text"
|
|
||||||
name="host"
|
|
||||||
placeholder="mobilizon.fr"
|
|
||||||
class="dark:text-black md:max-w-fit w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="">
|
|
||||||
<label v-translate class="inline font-bold" for="tagsAllOf">All of these tags</label>
|
|
||||||
<button
|
|
||||||
v-if="formTagsAllOf.length !== 0"
|
|
||||||
v-translate
|
|
||||||
class="text-sm ml-2"
|
|
||||||
@click="resetField('tagsAllOf')"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<vue-tags-input
|
|
||||||
v-model="formTagAllOf"
|
|
||||||
:placeholder="tagsPlaceholder"
|
|
||||||
:tags="formTagsAllOf"
|
|
||||||
@tags-changed="(newTags) => (formTagsAllOf = newTags)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<label v-translate class="inline font-bold" for="tagsOneOf">One of these tags</label>
|
|
||||||
<button
|
|
||||||
v-if="formTagsOneOf.length !== 0"
|
|
||||||
v-translate
|
|
||||||
class="text-sm ml-2"
|
|
||||||
@click="resetField('tagsOneOf')"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<vue-tags-input
|
|
||||||
v-model="formTagOneOf"
|
|
||||||
:placeholder="tagsPlaceholder"
|
|
||||||
:tags="formTagsOneOf"
|
|
||||||
@tags-changed="(newTags) => (formTagsOneOf = newTags)"
|
|
||||||
/>
|
|
||||||
</div>-->
|
|
||||||
|
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
<button
|
<button
|
||||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||||
|
@ -453,14 +302,11 @@
|
||||||
id="results-anchor"
|
id="results-anchor"
|
||||||
class="hidden sm:flex items-center justify-between dark:text-slate-100 mb-2"
|
class="hidden sm:flex items-center justify-between dark:text-slate-100 mb-2"
|
||||||
>
|
>
|
||||||
<p v-if="totalCount === 0">
|
<p v-if="searchLoading">{{ t("Loading search results...") }}</p>
|
||||||
<span
|
<p v-else-if="totalCount === 0">
|
||||||
v-if="
|
<span v-if="contentType === ContentType.EVENTS">{{
|
||||||
contentType === ContentType.EVENTS ||
|
t("No events found")
|
||||||
contentType === ContentType.SHORTEVENTS
|
}}</span>
|
||||||
"
|
|
||||||
>{{ t("No events found") }}</span
|
|
||||||
>
|
|
||||||
<span v-else-if="contentType === ContentType.LONGEVENTS">{{
|
<span v-else-if="contentType === ContentType.LONGEVENTS">{{
|
||||||
t("No activities found")
|
t("No activities found")
|
||||||
}}</span>
|
}}</span>
|
||||||
|
@ -470,12 +316,7 @@
|
||||||
<span v-else>{{ t("No results found") }}</span>
|
<span v-else>{{ t("No results found") }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
<span
|
<span v-if="contentType === ContentType.EVENTS">
|
||||||
v-if="
|
|
||||||
contentType === ContentType.EVENTS ||
|
|
||||||
contentType === ContentType.SHORTEVENTS
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
"{eventsCount} events found",
|
"{eventsCount} events found",
|
||||||
|
@ -517,12 +358,27 @@
|
||||||
t("Sort by")
|
t("Sort by")
|
||||||
}}</label>
|
}}</label>
|
||||||
<o-select
|
<o-select
|
||||||
:placeholder="t('Sort by')"
|
v-if="contentType !== ContentType.GROUPS"
|
||||||
v-model="sortBy"
|
:placeholder="t('Sort by events')"
|
||||||
id="sortOptionSelect"
|
v-model="sortByEvents"
|
||||||
|
id="sortOptionSelectEvents"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="sortOption in sortOptions"
|
v-for="sortOption in sortOptionsEvents"
|
||||||
|
:key="sortOption.key"
|
||||||
|
:value="sortOption.key"
|
||||||
|
>
|
||||||
|
{{ sortOption.label }}
|
||||||
|
</option>
|
||||||
|
</o-select>
|
||||||
|
<o-select
|
||||||
|
v-if="contentType === ContentType.GROUPS"
|
||||||
|
:placeholder="t('Sort by groups')"
|
||||||
|
v-model="sortByGroups"
|
||||||
|
id="sortOptionSelectGroups"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="sortOption in sortOptionsGroups"
|
||||||
:key="sortOption.key"
|
:key="sortOption.key"
|
||||||
:value="sortOption.key"
|
:value="sortOption.key"
|
||||||
>
|
>
|
||||||
|
@ -547,92 +403,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === ViewMode.LIST">
|
<div v-if="mode === ViewMode.LIST">
|
||||||
<template v-if="contentType === ContentType.ALL">
|
|
||||||
<template v-if="searchLoading">
|
|
||||||
<SkeletonGroupResultList v-for="i in 2" :key="i" />
|
|
||||||
<SkeletonEventResultList v-for="i in 4" :key="i" />
|
|
||||||
</template>
|
|
||||||
<o-notification v-if="features && !features.groups" variant="danger">
|
|
||||||
{{ t("Groups are not enabled on this instance.") }}
|
|
||||||
</o-notification>
|
|
||||||
<div v-else-if="searchGroups && searchGroups?.total > 0">
|
|
||||||
<GroupCard
|
|
||||||
class="my-2"
|
|
||||||
v-for="group in searchGroups?.elements"
|
|
||||||
:group="group"
|
|
||||||
:key="group.id"
|
|
||||||
:isRemoteGroup="group.__typename === 'GroupResult'"
|
|
||||||
:isLoggedIn="currentUser?.isLoggedIn"
|
|
||||||
mode="row"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchEvents && searchEvents.total > 0">
|
|
||||||
<event-card
|
|
||||||
mode="row"
|
|
||||||
v-for="event in searchEvents?.elements"
|
|
||||||
:event="event"
|
|
||||||
:key="event.uuid"
|
|
||||||
:options="{
|
|
||||||
isRemoteEvent: event.__typename === 'EventResult',
|
|
||||||
isLoggedIn: currentUser?.isLoggedIn,
|
|
||||||
}"
|
|
||||||
class="my-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<EmptyContent v-else-if="searchLoading === false" icon="magnify">
|
|
||||||
<span v-if="searchIsUrl">
|
|
||||||
{{ t("No event found at this address") }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="!search">
|
|
||||||
{{ t("No results found") }}
|
|
||||||
</span>
|
|
||||||
<i18n-t keypath="No results found for {search}" tag="span" v-else>
|
|
||||||
<template #search>
|
|
||||||
<b class="">{{ search }}</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
<template #desc v-if="searchIsUrl && !currentUser?.id">
|
|
||||||
{{
|
|
||||||
t(
|
|
||||||
"Only registered users may fetch remote events from their URL."
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</template>
|
|
||||||
<template #desc v-else>
|
|
||||||
<p class="my-2 text-start">
|
|
||||||
{{ t("Suggestions:") }}
|
|
||||||
</p>
|
|
||||||
<ul class="list-disc list-inside text-start">
|
|
||||||
<li>
|
|
||||||
{{ t("Make sure that all words are spelled correctly.") }}
|
|
||||||
</li>
|
|
||||||
<li>{{ t("Try different keywords.") }}</li>
|
|
||||||
<li>{{ t("Try more general keywords.") }}</li>
|
|
||||||
<li>{{ t("Try fewer keywords.") }}</li>
|
|
||||||
<li>{{ t("Change the filters.") }}</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</EmptyContent>
|
|
||||||
<o-pagination
|
|
||||||
v-if="
|
|
||||||
(searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT) ||
|
|
||||||
(searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT)
|
|
||||||
"
|
|
||||||
:total="
|
|
||||||
Math.max(searchEvents?.total ?? 0, searchGroups?.total ?? 0)
|
|
||||||
"
|
|
||||||
v-model:current="page"
|
|
||||||
:per-page="EVENT_PAGE_LIMIT"
|
|
||||||
:aria-next-label="t('Next page')"
|
|
||||||
:aria-previous-label="t('Previous page')"
|
|
||||||
:aria-page-label="t('Page')"
|
|
||||||
:aria-current-label="t('Current page')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-if="
|
||||||
contentType === ContentType.EVENTS ||
|
contentType === ContentType.EVENTS ||
|
||||||
contentType === ContentType.SHORTEVENTS ||
|
|
||||||
contentType === ContentType.LONGEVENTS
|
contentType === ContentType.LONGEVENTS
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
@ -771,7 +544,7 @@
|
||||||
:contentType="contentType"
|
:contentType="contentType"
|
||||||
:latitude="latitude"
|
:latitude="latitude"
|
||||||
:longitude="longitude"
|
:longitude="longitude"
|
||||||
:locationName="locationName"
|
:locationName="addressName"
|
||||||
@map-updated="setBounds"
|
@map-updated="setBounds"
|
||||||
:events="searchEvents"
|
:events="searchEvents"
|
||||||
:groups="searchGroups"
|
:groups="searchGroups"
|
||||||
|
@ -814,10 +587,6 @@ import {
|
||||||
enumTransformer,
|
enumTransformer,
|
||||||
booleanTransformer,
|
booleanTransformer,
|
||||||
} from "vue-use-route-query";
|
} from "vue-use-route-query";
|
||||||
import Calendar from "vue-material-design-icons/Calendar.vue";
|
|
||||||
import CalendarStar from "vue-material-design-icons/CalendarStar.vue";
|
|
||||||
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
|
||||||
import Magnify from "vue-material-design-icons/Magnify.vue";
|
|
||||||
|
|
||||||
import { useHead } from "@/utils/head";
|
import { useHead } from "@/utils/head";
|
||||||
import type { Locale } from "date-fns";
|
import type { Locale } from "date-fns";
|
||||||
|
@ -827,7 +596,6 @@ import langs from "@/i18n/langs.json";
|
||||||
import {
|
import {
|
||||||
useEventCategories,
|
useEventCategories,
|
||||||
useFeatures,
|
useFeatures,
|
||||||
useIsLongEvents,
|
|
||||||
useSearchConfig,
|
useSearchConfig,
|
||||||
} from "@/composition/apollo/config";
|
} from "@/composition/apollo/config";
|
||||||
import { coordsToGeoHash } from "@/utils/location";
|
import { coordsToGeoHash } from "@/utils/location";
|
||||||
|
@ -849,25 +617,25 @@ const EventMarkerMap = defineAsyncComponent(
|
||||||
|
|
||||||
const search = useRouteQuery("search", "");
|
const search = useRouteQuery("search", "");
|
||||||
const searchDebounced = refDebounced(search, 1000);
|
const searchDebounced = refDebounced(search, 1000);
|
||||||
const locationName = useRouteQuery("locationName", null);
|
const addressName = useRouteQuery("locationName", null);
|
||||||
const location = ref<IAddress | null>(null);
|
const address = ref<IAddress | null>(null);
|
||||||
|
|
||||||
watch(location, (newLocation) => {
|
watch(address, (newAddress: IAddress) => {
|
||||||
console.debug("location change", newLocation);
|
console.debug("address change", newAddress);
|
||||||
if (newLocation?.geom) {
|
if (newAddress?.geom) {
|
||||||
latitude.value = parseFloat(newLocation?.geom.split(";")[1]);
|
latitude.value = parseFloat(newAddress?.geom.split(";")[1]);
|
||||||
longitude.value = parseFloat(newLocation?.geom.split(";")[0]);
|
longitude.value = parseFloat(newAddress?.geom.split(";")[0]);
|
||||||
locationName.value = newLocation?.description;
|
addressName.value = newAddress?.description;
|
||||||
console.debug("set location", [
|
console.debug("set address", [
|
||||||
latitude.value,
|
latitude.value,
|
||||||
longitude.value,
|
longitude.value,
|
||||||
locationName.value,
|
addressName.value,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
console.debug("location emptied");
|
console.debug("address emptied");
|
||||||
latitude.value = undefined;
|
latitude.value = undefined;
|
||||||
longitude.value = undefined;
|
longitude.value = undefined;
|
||||||
locationName.value = null;
|
addressName.value = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -883,27 +651,17 @@ enum ViewMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventSortValues {
|
enum EventSortValues {
|
||||||
MATCH_DESC = "MATCH_DESC",
|
CREATED_AT_ASC = "CREATED_AT_ASC",
|
||||||
|
CREATED_AT_DESC = "CREATED_AT_DESC",
|
||||||
START_TIME_ASC = "START_TIME_ASC",
|
START_TIME_ASC = "START_TIME_ASC",
|
||||||
START_TIME_DESC = "START_TIME_DESC",
|
START_TIME_DESC = "START_TIME_DESC",
|
||||||
CREATED_AT_DESC = "CREATED_AT_DESC",
|
|
||||||
CREATED_AT_ASC = "CREATED_AT_ASC",
|
|
||||||
PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC",
|
PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC",
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GroupSortValues {
|
enum GroupSortValues {
|
||||||
MATCH_DESC = "MATCH_DESC",
|
|
||||||
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SortValues {
|
|
||||||
MATCH_DESC = "MATCH_DESC",
|
|
||||||
START_TIME_ASC = "START_TIME_ASC",
|
|
||||||
START_TIME_DESC = "START_TIME_DESC",
|
|
||||||
CREATED_AT_DESC = "CREATED_AT_DESC",
|
CREATED_AT_DESC = "CREATED_AT_DESC",
|
||||||
CREATED_AT_ASC = "CREATED_AT_ASC",
|
|
||||||
PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC",
|
|
||||||
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
|
MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC",
|
||||||
|
LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY",
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -911,24 +669,19 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
const tag = computed(() => props.tag);
|
const tag = computed(() => props.tag);
|
||||||
|
|
||||||
const page = useRouteQuery("page", 1, integerTransformer);
|
|
||||||
const eventPage = useRouteQuery("eventPage", 1, integerTransformer);
|
const eventPage = useRouteQuery("eventPage", 1, integerTransformer);
|
||||||
const groupPage = useRouteQuery("groupPage", 1, integerTransformer);
|
const groupPage = useRouteQuery("groupPage", 1, integerTransformer);
|
||||||
|
|
||||||
const latitude = useRouteQuery("lat", undefined, floatTransformer);
|
const latitude = useRouteQuery("lat", undefined, floatTransformer);
|
||||||
const longitude = useRouteQuery("lon", undefined, floatTransformer);
|
const longitude = useRouteQuery("lon", undefined, floatTransformer);
|
||||||
|
|
||||||
// TODO
|
|
||||||
// This should be updated with getRadiusFromLocal if we want to use user's
|
|
||||||
// preferences
|
|
||||||
const distance = useRouteQuery("distance", "10_km");
|
const distance = useRouteQuery("distance", "10_km");
|
||||||
const when = useRouteQuery("when", "any");
|
const when = useRouteQuery("when", "any");
|
||||||
const contentType = useRouteQuery(
|
const contentType = useRouteQuery(
|
||||||
"contentType",
|
"contentType",
|
||||||
tag.value ? ContentType.EVENTS : ContentType.ALL,
|
ContentType.EVENTS,
|
||||||
enumTransformer(ContentType)
|
enumTransformer(ContentType)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOnline = useRouteQuery("isOnline", false, booleanTransformer);
|
const isOnline = useRouteQuery("isOnline", false, booleanTransformer);
|
||||||
const categoryOneOf = useRouteQuery("categoryOneOf", [], arrayTransformer);
|
const categoryOneOf = useRouteQuery("categoryOneOf", [], arrayTransformer);
|
||||||
const statusOneOf = useRouteQuery(
|
const statusOneOf = useRouteQuery(
|
||||||
|
@ -937,16 +690,22 @@ const statusOneOf = useRouteQuery(
|
||||||
arrayTransformer
|
arrayTransformer
|
||||||
);
|
);
|
||||||
const languageOneOf = useRouteQuery("languageOneOf", [], arrayTransformer);
|
const languageOneOf = useRouteQuery("languageOneOf", [], arrayTransformer);
|
||||||
|
|
||||||
const searchTarget = useRouteQuery(
|
const searchTarget = useRouteQuery(
|
||||||
"target",
|
"target",
|
||||||
SearchTargets.INTERNAL,
|
SearchTargets.INTERNAL,
|
||||||
enumTransformer(SearchTargets)
|
enumTransformer(SearchTargets)
|
||||||
);
|
);
|
||||||
const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode));
|
const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode));
|
||||||
const sortBy = useRouteQuery(
|
const sortByEvents = useRouteQuery(
|
||||||
"sortBy",
|
"sortByEvents",
|
||||||
SortValues.START_TIME_ASC,
|
EventSortValues.START_TIME_ASC,
|
||||||
enumTransformer(SortValues)
|
enumTransformer(EventSortValues)
|
||||||
|
);
|
||||||
|
const sortByGroups = useRouteQuery(
|
||||||
|
"sortByGroups",
|
||||||
|
GroupSortValues.LAST_EVENT_ACTIVITY,
|
||||||
|
enumTransformer(GroupSortValues)
|
||||||
);
|
);
|
||||||
const bbox = useRouteQuery("bbox", undefined);
|
const bbox = useRouteQuery("bbox", undefined);
|
||||||
const zoom = useRouteQuery("zoom", undefined, integerTransformer);
|
const zoom = useRouteQuery("zoom", undefined, integerTransformer);
|
||||||
|
@ -957,7 +716,6 @@ const GROUP_PAGE_LIMIT = 16;
|
||||||
|
|
||||||
const { features } = useFeatures();
|
const { features } = useFeatures();
|
||||||
const { eventCategories } = useEventCategories();
|
const { eventCategories } = useEventCategories();
|
||||||
const { islongEvents } = useIsLongEvents();
|
|
||||||
|
|
||||||
const orderedCategories = computed(() => {
|
const orderedCategories = computed(() => {
|
||||||
if (!eventCategories.value) return [];
|
if (!eventCategories.value) return [];
|
||||||
|
@ -1070,113 +828,6 @@ const searchIsUrl = computed((): boolean => {
|
||||||
return url.protocol === "http:" || url.protocol === "https:";
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
});
|
});
|
||||||
|
|
||||||
const contentTypeMapping = computed(() => {
|
|
||||||
if (islongEvents.value) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
contentType: "ALL",
|
|
||||||
label: t("Everything"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
contentType: "SHORTEVENTS",
|
|
||||||
label: t("Events"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
contentType: "LONGEVENTS",
|
|
||||||
label: t("Activities"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
contentType: "GROUPS",
|
|
||||||
label: t("Groups"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
contentType: "ALL",
|
|
||||||
label: t("Everything"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
contentType: "EVENTS",
|
|
||||||
label: t("Events"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
contentType: "GROUPS",
|
|
||||||
label: t("Groups"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventDistance = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "anywhere",
|
|
||||||
label: t("Any distance"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5_km",
|
|
||||||
label: t(
|
|
||||||
"{number} kilometers",
|
|
||||||
{
|
|
||||||
number: 5,
|
|
||||||
},
|
|
||||||
5
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "10_km",
|
|
||||||
label: t(
|
|
||||||
"{number} kilometers",
|
|
||||||
{
|
|
||||||
number: 10,
|
|
||||||
},
|
|
||||||
10
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "25_km",
|
|
||||||
label: t(
|
|
||||||
"{number} kilometers",
|
|
||||||
{
|
|
||||||
number: 25,
|
|
||||||
},
|
|
||||||
25
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "50_km",
|
|
||||||
label: t(
|
|
||||||
"{number} kilometers",
|
|
||||||
{
|
|
||||||
number: 50,
|
|
||||||
},
|
|
||||||
50
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "100_km",
|
|
||||||
label: t(
|
|
||||||
"{number} kilometers",
|
|
||||||
{
|
|
||||||
number: 100,
|
|
||||||
},
|
|
||||||
100
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "150_km",
|
|
||||||
label: t(
|
|
||||||
"{number} kilometers",
|
|
||||||
{
|
|
||||||
number: 150,
|
|
||||||
},
|
|
||||||
150
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventStatuses = computed(() => {
|
const eventStatuses = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -1211,10 +862,21 @@ const geoHashLocation = computed(() =>
|
||||||
coordsToGeoHash(latitude.value, longitude.value)
|
coordsToGeoHash(latitude.value, longitude.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const radius = computed(() => Number.parseInt(distance.value.slice(0, -3)));
|
const radius = computed({
|
||||||
|
get(): number | null {
|
||||||
|
if (addressName.value) {
|
||||||
|
return Number.parseInt(distance.value.slice(0, -3));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(newRadius: number) {
|
||||||
|
distance.value = newRadius.toString() + "_km";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const longEvents = computed(() => {
|
const longEvents = computed(() => {
|
||||||
if (contentType.value === ContentType.SHORTEVENTS) {
|
if (contentType.value === ContentType.EVENTS) {
|
||||||
return false;
|
return false;
|
||||||
} else if (contentType.value === ContentType.LONGEVENTS) {
|
} else if (contentType.value === ContentType.LONGEVENTS) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1227,45 +889,44 @@ const totalCount = computed(() => {
|
||||||
return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0);
|
return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortOptions = computed(() => {
|
const sortOptionsGroups = computed(() => {
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
key: SortValues.MATCH_DESC,
|
key: GroupSortValues.LAST_EVENT_ACTIVITY,
|
||||||
label: t("Best match"),
|
label: t("Last event activity"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: GroupSortValues.MEMBER_COUNT_DESC,
|
||||||
|
label: t("Decreasing number of members"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: GroupSortValues.CREATED_AT_DESC,
|
||||||
|
label: t("Decreasing creation date"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
return options;
|
||||||
contentType.value === ContentType.EVENTS ||
|
});
|
||||||
contentType.value === ContentType.SHORTEVENTS ||
|
|
||||||
contentType.value === ContentType.LONGEVENTS
|
|
||||||
) {
|
|
||||||
options.push(
|
|
||||||
{
|
|
||||||
key: SortValues.START_TIME_ASC,
|
|
||||||
label: t("Event date"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SortValues.CREATED_AT_DESC,
|
|
||||||
label: t("Most recently published"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SortValues.CREATED_AT_ASC,
|
|
||||||
label: t("Least recently published"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SortValues.PARTICIPANT_COUNT_DESC,
|
|
||||||
label: t("With the most participants"),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType.value === ContentType.GROUPS) {
|
const sortOptionsEvents = computed(() => {
|
||||||
options.push({
|
const options = [
|
||||||
key: SortValues.MEMBER_COUNT_DESC,
|
{
|
||||||
label: t("Number of members"),
|
key: EventSortValues.START_TIME_ASC,
|
||||||
});
|
label: t("Event date"),
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
key: EventSortValues.CREATED_AT_DESC,
|
||||||
|
label: t("Most recently published"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: EventSortValues.CREATED_AT_ASC,
|
||||||
|
label: t("Least recently published"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: EventSortValues.PARTICIPANT_COUNT_DESC,
|
||||||
|
label: t("With the most participants"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
@ -1283,7 +944,7 @@ const handleSearchConfigChanged = (
|
||||||
searchConfigChanged?.global?.isEnabled &&
|
searchConfigChanged?.global?.isEnabled &&
|
||||||
searchConfigChanged?.global?.isDefault
|
searchConfigChanged?.global?.isDefault
|
||||||
) {
|
) {
|
||||||
searchTarget.value = SearchTargets.GLOBAL;
|
searchTarget.value = SearchTargets.INTERNAL;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1321,11 +982,11 @@ watch(isOnline, (newIsOnline) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortByForType = (
|
const sortByForType = (
|
||||||
value: SortValues,
|
value: EventSortValues,
|
||||||
allowed: typeof EventSortValues | typeof GroupSortValues
|
allowed: typeof EventSortValues
|
||||||
): SortValues | undefined => {
|
): EventSortValues | undefined => {
|
||||||
if (value === SortValues.START_TIME_ASC && when.value === "past") {
|
if (value === EventSortValues.START_TIME_ASC && when.value === "past") {
|
||||||
value = SortValues.START_TIME_DESC;
|
value = EventSortValues.START_TIME_DESC;
|
||||||
}
|
}
|
||||||
return Object.values(allowed).includes(value) ? value : undefined;
|
return Object.values(allowed).includes(value) ? value : undefined;
|
||||||
};
|
};
|
||||||
|
@ -1360,20 +1021,15 @@ watch(
|
||||||
searchTarget,
|
searchTarget,
|
||||||
bbox,
|
bbox,
|
||||||
zoom,
|
zoom,
|
||||||
sortBy,
|
sortByEvents,
|
||||||
|
sortByGroups,
|
||||||
boostLanguagesQuery,
|
boostLanguagesQuery,
|
||||||
],
|
],
|
||||||
([newContentType]) => {
|
([newContentType]) => {
|
||||||
switch (newContentType) {
|
switch (newContentType) {
|
||||||
case ContentType.ALL:
|
|
||||||
page.value = 1;
|
|
||||||
break;
|
|
||||||
case ContentType.EVENTS:
|
case ContentType.EVENTS:
|
||||||
eventPage.value = 1;
|
eventPage.value = 1;
|
||||||
break;
|
break;
|
||||||
case ContentType.SHORTEVENTS:
|
|
||||||
eventPage.value = 1;
|
|
||||||
break;
|
|
||||||
case ContentType.LONGEVENTS:
|
case ContentType.LONGEVENTS:
|
||||||
eventPage.value = 1;
|
eventPage.value = 1;
|
||||||
break;
|
break;
|
||||||
|
@ -1393,12 +1049,10 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
|
||||||
location: geoHashLocation.value,
|
location: geoHashLocation.value,
|
||||||
beginsOn: start.value,
|
beginsOn: start.value,
|
||||||
endsOn: end.value,
|
endsOn: end.value,
|
||||||
longevents: longEvents.value,
|
longEvents: longEvents.value,
|
||||||
radius: geoHashLocation.value ? radius.value : undefined,
|
radius: geoHashLocation.value ? radius.value : undefined,
|
||||||
eventPage:
|
eventPage: eventPage.value,
|
||||||
contentType.value === ContentType.ALL ? page.value : eventPage.value,
|
groupPage: groupPage.value,
|
||||||
groupPage:
|
|
||||||
contentType.value === ContentType.ALL ? page.value : groupPage.value,
|
|
||||||
limit: EVENT_PAGE_LIMIT,
|
limit: EVENT_PAGE_LIMIT,
|
||||||
type: isOnline.value ? "ONLINE" : undefined,
|
type: isOnline.value ? "ONLINE" : undefined,
|
||||||
categoryOneOf: categoryOneOf.value,
|
categoryOneOf: categoryOneOf.value,
|
||||||
|
@ -1407,8 +1061,8 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
|
||||||
searchTarget: searchTarget.value,
|
searchTarget: searchTarget.value,
|
||||||
bbox: mode.value === ViewMode.MAP ? bbox.value : undefined,
|
bbox: mode.value === ViewMode.MAP ? bbox.value : undefined,
|
||||||
zoom: zoom.value,
|
zoom: zoom.value,
|
||||||
sortByEvents: sortByForType(sortBy.value, EventSortValues),
|
sortByEvents: sortByForType(sortByEvents.value, EventSortValues),
|
||||||
sortByGroups: sortByForType(sortBy.value, GroupSortValues),
|
sortByGroups: sortByGroups.value,
|
||||||
boostLanguages: boostLanguagesQuery.value,
|
boostLanguages: boostLanguagesQuery.value,
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -147,7 +147,7 @@ import RouteName from "../../router/name";
|
||||||
import { AddressSearchType } from "@/types/enums";
|
import { AddressSearchType } from "@/types/enums";
|
||||||
import { Address, IAddress } from "@/types/address.model";
|
import { Address, IAddress } from "@/types/address.model";
|
||||||
import { useTimezones } from "@/composition/apollo/config";
|
import { useTimezones } from "@/composition/apollo/config";
|
||||||
import { useUserSettings, updateLocale } from "@/composition/apollo/user";
|
import { useLoggedUser, updateLocale } from "@/composition/apollo/user";
|
||||||
import { useHead } from "@/utils/head";
|
import { useHead } from "@/utils/head";
|
||||||
import { computed, defineAsyncComponent, ref, watch } from "vue";
|
import { computed, defineAsyncComponent, ref, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
@ -159,7 +159,7 @@ const FullAddressAutoComplete = defineAsyncComponent(
|
||||||
|
|
||||||
const { timezones: serverTimezones, loading: loadingTimezones } =
|
const { timezones: serverTimezones, loading: loadingTimezones } =
|
||||||
useTimezones();
|
useTimezones();
|
||||||
const { loggedUser, loading: loadingUserSettings } = useUserSettings();
|
const { loggedUser, loading: loadingUserSettings } = useLoggedUser();
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
|
2
test/fixtures/nodeinfo/data.json
vendored
2
test/fixtures/nodeinfo/data.json
vendored
|
@ -22,7 +22,7 @@
|
||||||
},
|
},
|
||||||
"software": {
|
"software": {
|
||||||
"name": "Mobilizon",
|
"name": "Mobilizon",
|
||||||
"version": "5.0.0-beta.1",
|
"version": "5.1.0",
|
||||||
"repository": "https://framagit.org/framasoft/mobilizon"
|
"repository": "https://framagit.org/framasoft/mobilizon"
|
||||||
},
|
},
|
||||||
"openRegistrations": true
|
"openRegistrations": true
|
||||||
|
|
|
@ -56,7 +56,8 @@ defmodule Mobilizon.GraphQL.API.SearchTest do
|
||||||
current_actor_id: nil,
|
current_actor_id: nil,
|
||||||
exclude_my_groups: false,
|
exclude_my_groups: false,
|
||||||
exclude_stale_actors: true,
|
exclude_stale_actors: true,
|
||||||
local_only: false
|
local_only: false,
|
||||||
|
sort_by: nil
|
||||||
],
|
],
|
||||||
1,
|
1,
|
||||||
10
|
10
|
||||||
|
|
|
@ -4,6 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|
||||||
import Swoosh.TestAssertions
|
import Swoosh.TestAssertions
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Config
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Federation.ActivityPub.Relay
|
alias Mobilizon.Federation.ActivityPub.Relay
|
||||||
alias Mobilizon.Reports.{Note, Report}
|
alias Mobilizon.Reports.{Note, Report}
|
||||||
|
@ -568,7 +569,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
|
||||||
assert_email_sent(
|
assert_email_sent(
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject:
|
subject:
|
||||||
"An administrator manually changed the email attached to your account on Test instance"
|
"An administrator manually changed the email attached to your account on #{Config.instance_name()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# # Swoosh.TestAssertions can't test multiple emails sent
|
# # Swoosh.TestAssertions can't test multiple emails sent
|
||||||
|
|
|
@ -113,9 +113,11 @@ defmodule Mobilizon.GraphQL.Resolvers.ConfigTest do
|
||||||
assert res["data"]["config"]["long_description"] == nil
|
assert res["data"]["config"]["long_description"] == nil
|
||||||
assert res["data"]["config"]["slogan"] == nil
|
assert res["data"]["config"]["slogan"] == nil
|
||||||
assert res["data"]["config"]["languages"] == []
|
assert res["data"]["config"]["languages"] == []
|
||||||
assert length(res["data"]["config"]["timezones"]) == 596
|
assert length(res["data"]["config"]["timezones"]) > 500
|
||||||
assert res["data"]["config"]["rules"] == nil
|
assert res["data"]["config"]["rules"] == nil
|
||||||
assert String.slice(res["data"]["config"]["version"], 0, 5) == "5.0.0"
|
# there is no real way to make this test work when bumping instance version
|
||||||
|
# as there is no tag with this new version number in the git history, yet
|
||||||
|
# assert String.slice(res["data"]["config"]["version"], 0, 5) == "5.1.0"
|
||||||
assert res["data"]["config"]["federating"] == true
|
assert res["data"]["config"]["federating"] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
|
|
||||||
describe "search events/3" do
|
describe "search events/3" do
|
||||||
@search_events_query """
|
@search_events_query """
|
||||||
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime, $longevents:Boolean, $searchTarget: SearchTarget) {
|
query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime, $longEvents:Boolean, $searchTarget: SearchTarget) {
|
||||||
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn, longevents: $longevents, searchTarget: $searchTarget) {
|
searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn, longEvents: $longEvents, searchTarget: $searchTarget) {
|
||||||
total,
|
total,
|
||||||
elements {
|
elements {
|
||||||
id
|
id
|
||||||
|
@ -224,7 +224,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
res =
|
res =
|
||||||
AbsintheHelpers.graphql_query(conn,
|
AbsintheHelpers.graphql_query(conn,
|
||||||
query: @search_events_query,
|
query: @search_events_query,
|
||||||
variables: %{longevents: false}
|
variables: %{longEvents: false}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
@ -241,7 +241,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
res =
|
res =
|
||||||
AbsintheHelpers.graphql_query(conn,
|
AbsintheHelpers.graphql_query(conn,
|
||||||
query: @search_events_query,
|
query: @search_events_query,
|
||||||
variables: %{longevents: true}
|
variables: %{longEvents: true}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
@ -296,7 +296,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
res =
|
res =
|
||||||
AbsintheHelpers.graphql_query(conn,
|
AbsintheHelpers.graphql_query(conn,
|
||||||
query: @search_events_query,
|
query: @search_events_query,
|
||||||
variables: %{longevents: false}
|
variables: %{longEvents: false}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
@ -311,7 +311,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
res =
|
res =
|
||||||
AbsintheHelpers.graphql_query(conn,
|
AbsintheHelpers.graphql_query(conn,
|
||||||
query: @search_events_query,
|
query: @search_events_query,
|
||||||
variables: %{longevents: true}
|
variables: %{longEvents: true}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
@ -364,7 +364,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
res =
|
res =
|
||||||
AbsintheHelpers.graphql_query(conn,
|
AbsintheHelpers.graphql_query(conn,
|
||||||
query: @search_events_query,
|
query: @search_events_query,
|
||||||
variables: %{longevents: false}
|
variables: %{longEvents: false}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
@ -378,7 +378,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
|
||||||
res =
|
res =
|
||||||
AbsintheHelpers.graphql_query(conn,
|
AbsintheHelpers.graphql_query(conn,
|
||||||
query: @search_events_query,
|
query: @search_events_query,
|
||||||
variables: %{longevents: true}
|
variables: %{longEvents: true}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
|
|
|
@ -11,7 +11,7 @@ defmodule Mobilizon.Service.Metadata.InstanceTest do
|
||||||
|
|
||||||
assert Instance.build_tags() |> Utils.stringify_tags() ==
|
assert Instance.build_tags() |> Utils.stringify_tags() ==
|
||||||
"""
|
"""
|
||||||
<title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script>\n<link href=\"#{Endpoint.url()}/feed/instance/atom\" rel=\"alternate\" title=\"Test instance's feed\" type=\"application/atom+xml\"><link href=\"#{Endpoint.url()}/feed/instance/ics\" rel=\"alternate\" title=\"Test instance's feed\" type=\"text/calendar\">\
|
<title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script>\n<link href=\"#{Endpoint.url()}/feed/instance/atom\" rel=\"alternate\" title=\"#{Config.instance_name()}'s feed\" type=\"application/atom+xml\"><link href=\"#{Endpoint.url()}/feed/instance/ics\" rel=\"alternate\" title=\"#{Config.instance_name()}'s feed\" type=\"text/calendar\">\
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -115,11 +115,12 @@ defmodule Mobilizon.Web.FeedControllerTest do
|
||||||
begins_on: DateTime.add(DateTime.utc_now(), 4, :day)
|
begins_on: DateTime.add(DateTime.utc_now(), 4, :day)
|
||||||
)
|
)
|
||||||
|
|
||||||
event3 =
|
event_in_the_past =
|
||||||
insert(:event,
|
insert(:event,
|
||||||
organizer_actor: actor,
|
organizer_actor: actor,
|
||||||
attributed_to: group,
|
attributed_to: group,
|
||||||
title: "Event Three",
|
title: "Event Three",
|
||||||
|
ends_on: DateTime.add(DateTime.utc_now(), -3, :day),
|
||||||
begins_on: DateTime.add(DateTime.utc_now(), -2, :day)
|
begins_on: DateTime.add(DateTime.utc_now(), -2, :day)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -135,7 +136,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
|
||||||
|
|
||||||
Enum.each(entries, fn entry ->
|
Enum.each(entries, fn entry ->
|
||||||
assert entry.summary in [event1.title, event2.title]
|
assert entry.summary in [event1.title, event2.title]
|
||||||
refute entry.summary == event3.title
|
refute entry.summary == event_in_the_past.title
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert entry1.categories == [tag1.title]
|
assert entry1.categories == [tag1.title]
|
||||||
|
|
|
@ -29,7 +29,6 @@ const createSlotButtonText = "+ Create a post";
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
privateSection?: boolean;
|
|
||||||
route?: { name: string; params: { preferredUsername: string } };
|
route?: { name: string; params: { preferredUsername: string } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,10 +72,6 @@ describe("GroupSection", () => {
|
||||||
|
|
||||||
expect(wrapper.find("a").attributes("href")).toBe(`/@${groupUsername}/p`);
|
expect(wrapper.find("a").attributes("href")).toBe(`/@${groupUsername}/p`);
|
||||||
|
|
||||||
// expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
|
|
||||||
// true
|
|
||||||
// );
|
|
||||||
|
|
||||||
expect(wrapper.find("section > div.flex-1").text()).toBe(defaultSlotText);
|
expect(wrapper.find("section > div.flex-1").text()).toBe(defaultSlotText);
|
||||||
expect(wrapper.find(".flex.justify-end.p-2 a").text()).toBe(
|
expect(wrapper.find(".flex.justify-end.p-2 a").text()).toBe(
|
||||||
createSlotButtonText
|
createSlotButtonText
|
||||||
|
@ -88,11 +83,8 @@ describe("GroupSection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders public group section", () => {
|
it("renders public group section", () => {
|
||||||
const wrapper = generateWrapper({ privateSection: false });
|
const wrapper = generateWrapper();
|
||||||
|
|
||||||
// expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
|
|
||||||
// false
|
|
||||||
// );
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue