diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 67d85eb2..cb881a3d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,7 +17,7 @@ jobs: env: POSTGRES_PASSWORD: postgres ports: - - 5432:5432 + - 5433:5432 options: >- --health-cmd pg_isready --health-interval 10s diff --git a/Profile.ci-test b/Profile.ci-test new file mode 100644 index 00000000..0c9315b7 --- /dev/null +++ b/Profile.ci-test @@ -0,0 +1,2 @@ +vite-ssr: bin/vite ssr +rails: sleep 1 && bin/rails db:prepare && bin/rails test:all || exit -1 diff --git a/app/components/JourneyAppLayout.tsx b/app/components/JourneysAppLayout.tsx similarity index 86% rename from app/components/JourneyAppLayout.tsx rename to app/components/JourneysAppLayout.tsx index 44c8eaf1..2f63d67e 100644 --- a/app/components/JourneyAppLayout.tsx +++ b/app/components/JourneysAppLayout.tsx @@ -6,8 +6,8 @@ import type { AppShellProps, ContainerProps, MantineSize } from "@mantine/core"; import type { Maybe } from "~/helpers/graphql"; import type { AppViewerFragment } from "~/helpers/graphql"; -import JourneyAppMeta from "./JourneyAppMeta"; -import type { JourneyAppMetaProps } from "./JourneyAppMeta"; +import JourneysAppMeta from "./JourneysAppMeta"; +import type { JourneysAppMetaProps } from "./JourneysAppMeta"; // import AppMenu from "./AppMenu"; import AppFlash from "./AppFlash"; @@ -17,10 +17,10 @@ import PageLayout from "./PageLayout"; import "./AppLayout.css"; import classes from "./AppLayout.module.css"; -export type JourneyAppLayoutProps = JourneyAppMetaProps & +export type JourneysAppLayoutProps = JourneysAppMetaProps & AppShellProps & { readonly viewer: Maybe; - readonly breadcrumbs?: ReadonlyArray; + readonly breadcrumbs?: ReadonlyArray; readonly withContainer?: boolean; readonly containerSize?: MantineSize | (string & {}) | number; readonly containerProps?: ContainerProps; @@ -28,12 +28,12 @@ export type JourneyAppLayoutProps = JourneyAppMetaProps & readonly gutterSize?: MantineSize | (string & {}) | number; }; -export type JourneyAppBreadcrumb = { +export type JourneysAppBreadcrumb = { readonly title: string; readonly href: string; }; -const JourneyAppLayout: FC = ({ +const JourneysAppLayout: FC = ({ // viewer, title, description, @@ -51,7 +51,7 @@ const JourneyAppLayout: FC = ({ }) => { // == Breadcrumbs const filteredBreadcrumbs = useMemo( - () => (breadcrumbs?.filter(x => !!x) || []) as JourneyAppBreadcrumb[], + () => (breadcrumbs?.filter(x => !!x) || []) as JourneysAppBreadcrumb[], [breadcrumbs], ); @@ -74,7 +74,7 @@ const JourneyAppLayout: FC = ({ return ( - + = ({ fz="md" className={classes.logo} > - Journey + Journeys {/* */} @@ -155,4 +155,4 @@ const JourneyAppLayout: FC = ({ ); }; -export default JourneyAppLayout; +export default JourneysAppLayout; diff --git a/app/components/JourneyAppMeta.tsx b/app/components/JourneysAppMeta.tsx similarity index 92% rename from app/components/JourneyAppMeta.tsx rename to app/components/JourneysAppMeta.tsx index 1368eb99..01d3f3f0 100644 --- a/app/components/JourneyAppMeta.tsx +++ b/app/components/JourneysAppMeta.tsx @@ -1,19 +1,19 @@ import type { FC } from "react"; const JourneyAppMetaSiteType = "website"; -const JourneyAppMetaSiteName = "Journey"; +const JourneyAppMetaSiteName = "Journeys"; const JourneyAppMetaSiteDescription = undefined; // "Welcome to my little corner of the internet :)"; const JourneyAppMetaSiteImage = undefined; // "/banner.png"; const JourneyAppMetaTitleSeparator = "|"; -export type JourneyAppMetaProps = { +export type JourneysAppMetaProps = { readonly title?: string | string[]; readonly description?: string | null; readonly imageUrl?: string | null; readonly noIndex?: boolean; }; -const JourneyAppMeta: FC = ({ +const JourneysAppMeta: FC = ({ title: titleProp, description = JourneyAppMetaSiteDescription, imageUrl = JourneyAppMetaSiteImage, @@ -60,4 +60,4 @@ const JourneyAppMeta: FC = ({ ); }; -export default JourneyAppMeta; +export default JourneysAppMeta; diff --git a/app/constraints/journey_subdomain_constraint.rb b/app/constraints/journeys_subdomain_constraint.rb similarity index 82% rename from app/constraints/journey_subdomain_constraint.rb rename to app/constraints/journeys_subdomain_constraint.rb index d3f3efd0..9d2fa011 100644 --- a/app/constraints/journey_subdomain_constraint.rb +++ b/app/constraints/journeys_subdomain_constraint.rb @@ -1,13 +1,13 @@ # typed: strict # frozen_string_literal: true -class JourneySubdomainConstraint +class JourneysSubdomainConstraint extend T::Sig sig { params(request: ActionDispatch::Request).returns(T::Boolean) } def matches?(request) subdomain = T.let(request.subdomain.dup, String) subdomain.delete_suffix!(".127.0.0.1") if Rails.env.development? - subdomain == "journey" + subdomain == "journeys" end end diff --git a/app/controllers/journey/application_controller.rb b/app/controllers/journeys/application_controller.rb similarity index 88% rename from app/controllers/journey/application_controller.rb rename to app/controllers/journeys/application_controller.rb index 2853a8f5..24ab8893 100644 --- a/app/controllers/journey/application_controller.rb +++ b/app/controllers/journeys/application_controller.rb @@ -1,11 +1,14 @@ # typed: strict # frozen_string_literal: true -module Journey +module Journeys class ApplicationController < ::ApplicationController # == Filters before_action :set_participant_id + # == Authorization + authorize :participant_id + private # == Helpers diff --git a/app/controllers/journey/home_controller.rb b/app/controllers/journeys/home_controller.rb similarity index 82% rename from app/controllers/journey/home_controller.rb rename to app/controllers/journeys/home_controller.rb index 10c954f9..dd8231f2 100644 --- a/app/controllers/journey/home_controller.rb +++ b/app/controllers/journeys/home_controller.rb @@ -1,7 +1,7 @@ # typed: true # frozen_string_literal: true -module Journey +module Journeys class HomeController < ApplicationController # == Filters before_action :set_active_session @@ -9,9 +9,9 @@ class HomeController < ApplicationController # == Actions def show if (session = @active_session) - redirect_to(journey_session_path(session)) + redirect_to(journeys_session_path(session)) else - render(inertia: "JourneyHomePage") + render(inertia: "JourneysHomePage") end end diff --git a/app/controllers/journey/sessions_controller.rb b/app/controllers/journeys/sessions_controller.rb similarity index 79% rename from app/controllers/journey/sessions_controller.rb rename to app/controllers/journeys/sessions_controller.rb index 4eb2c926..98f3924c 100644 --- a/app/controllers/journey/sessions_controller.rb +++ b/app/controllers/journeys/sessions_controller.rb @@ -1,24 +1,25 @@ # typed: true # frozen_string_literal: true -module Journey +module Journeys class SessionsController < ApplicationController # == Filters - before_action :set_session_and_participation, only: :show + before_action :set_session, only: :show # == Actions def show session = @session or raise "Missing session" - if @participation - data = query!("JourneySessionPageQuery", { + if allowed_to?(:show?, session) + data = query!("JourneysSessionPageQuery", { session_id: session.to_gid.to_s, }) - render(inertia: "JourneySessionPage", props: { + render(inertia: "JourneysSessionPage", props: { + homepage_url: journeys_root_url, data:, }) else redirect_to( - journey_root_path, + journeys_root_path, alert: "You are not a participant in this session.", ) end @@ -33,10 +34,10 @@ def create **participation_params, ) session.save! - redirect_to(journey_session_path(session)) + redirect_to(journeys_session_path(session)) rescue => error redirect_to( - journey_root_path, + journeys_root_path, alert: "Failed to start session: #{error.message}", ) end @@ -80,15 +81,11 @@ def transcribe_goal_recording # == Filter Handlers sig { void } - def set_session_and_participation + def set_session @session = T.let( Session.friendly.find(params.fetch(:id)), T.nilable(Session), ) - @participation = T.let( - SessionParticipation.find_by(session: @session, participant_id:), - T.nilable(SessionParticipation), - ) end end end diff --git a/app/graphql/concerns/resolver.rb b/app/graphql/concerns/resolver.rb index b372e95e..3c52c734 100644 --- a/app/graphql/concerns/resolver.rb +++ b/app/graphql/concerns/resolver.rb @@ -4,6 +4,7 @@ module Resolver extend T::Sig extend T::Helpers + extend ActiveSupport::Concern include Routing # == Annotations @@ -77,7 +78,7 @@ def actor_id end sig { returns(String) } - def journey_participant_id + def journeys_participant_id cookies.signed[:journey_participant_id] or raise "Missing journey participant ID" end diff --git a/app/graphql/queries/journeys_base_query.rb b/app/graphql/queries/journeys_base_query.rb new file mode 100644 index 00000000..26ab61fa --- /dev/null +++ b/app/graphql/queries/journeys_base_query.rb @@ -0,0 +1,9 @@ +# typed: strict +# frozen_string_literal: true + +module Queries + class JourneysBaseQuery < BaseQuery + # == Authorization + authorize :participant_id, through: :journeys_participant_id + end +end diff --git a/app/graphql/queries/journey_session.rb b/app/graphql/queries/journeys_session.rb similarity index 51% rename from app/graphql/queries/journey_session.rb rename to app/graphql/queries/journeys_session.rb index b234350a..3ce56bf4 100644 --- a/app/graphql/queries/journey_session.rb +++ b/app/graphql/queries/journeys_session.rb @@ -2,19 +2,19 @@ # frozen_string_literal: true module Queries - class JourneySession < BaseQuery + class JourneysSession < JourneysBaseQuery include AllowsFailedLoads # == Type - type Types::JourneySessionType, null: true + type Types::JourneysSessionType, null: true # == Arguments - argument :id, ID, loads: Types::JourneySessionType, as: :session + argument :id, ID, loads: Types::JourneysSessionType, as: :session # == Resolver sig do - params(session: T.nilable(::Journey::Session)) - .returns(T.nilable(::Journey::Session)) + params(session: T.nilable(::Journeys::Session)) + .returns(T.nilable(::Journeys::Session)) end def resolve(session:) return unless session diff --git a/app/graphql/subscriptions/journey_session_participation.rb b/app/graphql/subscriptions/journey_session_participation.rb deleted file mode 100644 index 76f4cc6b..00000000 --- a/app/graphql/subscriptions/journey_session_participation.rb +++ /dev/null @@ -1,22 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module Subscriptions - class JourneySessionParticipation < BaseSubscription - # == Configuration - broadcastable false - - # == Type - type Types::JourneySessionParticipationType, null: true - - # == Arguments - argument :session_id, ID, loads: Types::JourneySessionType - - # == Callback Handlers - sig do - params(session: ::Journey::Session) - .returns(T.nilable(::Journey::SessionParticipation)) - end - def subscribe(session:) = nil - end -end diff --git a/app/graphql/subscriptions/journeys_session_participation.rb b/app/graphql/subscriptions/journeys_session_participation.rb new file mode 100644 index 00000000..1899ea00 --- /dev/null +++ b/app/graphql/subscriptions/journeys_session_participation.rb @@ -0,0 +1,22 @@ +# typed: strict +# frozen_string_literal: true + +module Subscriptions + class JourneysSessionParticipation < BaseSubscription + # == Configuration + broadcastable false + + # == Type + type Types::JourneysSessionParticipationType, null: true + + # == Arguments + argument :session_id, ID, loads: Types::JourneysSessionType + + # == Callback Handlers + sig do + params(session: ::Journeys::Session) + .returns(T.nilable(::Journeys::SessionParticipation)) + end + def subscribe(session:) = nil + end +end diff --git a/app/graphql/types/journey_session_participation_type.rb b/app/graphql/types/journeys_session_participation_type.rb similarity index 63% rename from app/graphql/types/journey_session_participation_type.rb rename to app/graphql/types/journeys_session_participation_type.rb index af33cf97..01cadfd5 100644 --- a/app/graphql/types/journey_session_participation_type.rb +++ b/app/graphql/types/journeys_session_participation_type.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module Types - class JourneySessionParticipationType < BaseObject + class JourneysSessionParticipationType < BaseObject # == Interfaces implements NodeType @@ -10,16 +10,16 @@ class JourneySessionParticipationType < BaseObject field :goal, String, null: false field :participant_is_viewer, Boolean, null: false field :participant_name, String, null: false - field :session, [JourneySessionType], null: false + field :session, [JourneysSessionType], null: false # == Resolvers sig { returns(T::Boolean) } def participant_is_viewer - journey_participant_id == object.participant_id + journeys_participant_id == object.participant_id end # == Helpers - sig { override.returns(::Journey::SessionParticipation) } + sig { override.returns(::Journeys::SessionParticipation) } def object = super end end diff --git a/app/graphql/types/journey_session_type.rb b/app/graphql/types/journeys_session_type.rb similarity index 63% rename from app/graphql/types/journey_session_type.rb rename to app/graphql/types/journeys_session_type.rb index 0453877a..ebf32c4e 100644 --- a/app/graphql/types/journey_session_type.rb +++ b/app/graphql/types/journeys_session_type.rb @@ -2,23 +2,23 @@ # frozen_string_literal: true module Types - class JourneySessionType < BaseObject + class JourneysSessionType < BaseObject # == Interfaces implements NodeType # == Fields - field :participations, [JourneySessionParticipationType], null: false + field :participations, [JourneysSessionParticipationType], null: false field :started_at, DateTimeType, null: false, method: :created_at field :url, String, null: false # == Resolvers sig { returns(String) } def url - journey_session_url(object) + journeys_session_url(object) end # == Helpers - sig { override.returns(::Journey::Session) } + sig { override.returns(::Journeys::Session) } def object = super end end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 65409aea..3d3e89ac 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -34,7 +34,7 @@ class QueryType < BaseObject field :journal_entry, resolver: Queries::JournalEntry field :journal_entry_comments, resolver: Queries::JournalEntryComments - field :journey_session, resolver: Queries::JourneySession + field :journeys_session, resolver: Queries::JourneysSession field :location_access_grants, resolver: Queries::LocationAccessGrants field :pensieve_messages, resolver: Queries::PensieveMessages end diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb index 2c5184fa..2ba0acf3 100644 --- a/app/graphql/types/subscription_type.rb +++ b/app/graphql/types/subscription_type.rb @@ -6,8 +6,8 @@ class SubscriptionType < BaseObject # == Subscriptions field :activity_status, subscription: Subscriptions::ActivityStatus field :currently_playing, subscription: Subscriptions::CurrentlyPlaying - field :journey_session_participation, - subscription: Subscriptions::JourneySessionParticipation + field :journeys_session_participation, + subscription: Subscriptions::JourneysSessionParticipation field :location, subscription: Subscriptions::Location field :pensieve_message, subscription: Subscriptions::PensieveMessage field :test_subscription, subscription: Subscriptions::TestSubscription diff --git a/app/helpers/apollo/clientHelpers.generated.ts b/app/helpers/apollo/clientHelpers.generated.ts index 926b5879..544df8ab 100644 --- a/app/helpers/apollo/clientHelpers.generated.ts +++ b/app/helpers/apollo/clientHelpers.generated.ts @@ -86,15 +86,15 @@ export type JournalEntryFieldPolicy = { title?: FieldPolicy | FieldReadFunction, url?: FieldPolicy | FieldReadFunction }; -export type JourneySessionKeySpecifier = ('id' | 'participations' | 'startedAt' | 'url' | JourneySessionKeySpecifier)[]; -export type JourneySessionFieldPolicy = { +export type JourneysSessionKeySpecifier = ('id' | 'participations' | 'startedAt' | 'url' | JourneysSessionKeySpecifier)[]; +export type JourneysSessionFieldPolicy = { id?: FieldPolicy | FieldReadFunction, participations?: FieldPolicy | FieldReadFunction, startedAt?: FieldPolicy | FieldReadFunction, url?: FieldPolicy | FieldReadFunction }; -export type JourneySessionParticipationKeySpecifier = ('goal' | 'id' | 'participantIsViewer' | 'participantName' | 'session' | JourneySessionParticipationKeySpecifier)[]; -export type JourneySessionParticipationFieldPolicy = { +export type JourneysSessionParticipationKeySpecifier = ('goal' | 'id' | 'participantIsViewer' | 'participantName' | 'session' | JourneysSessionParticipationKeySpecifier)[]; +export type JourneysSessionParticipationFieldPolicy = { goal?: FieldPolicy | FieldReadFunction, id?: FieldPolicy | FieldReadFunction, participantIsViewer?: FieldPolicy | FieldReadFunction, @@ -197,7 +197,7 @@ export type PensieveMessageFieldPolicy = { text?: FieldPolicy | FieldReadFunction, timestamp?: FieldPolicy | FieldReadFunction }; -export type QueryKeySpecifier = ('activityStatus' | 'announcement' | 'bootedAt' | 'contactEmail' | 'currentlyPlaying' | 'explorations' | 'googleCredentials' | 'icloudCredentials' | 'imageBySignedId' | 'instagramCredentials' | 'journalEntry' | 'journalEntryComments' | 'journeySession' | 'location' | 'locationAccessGrants' | 'passwordStrength' | 'pensieveMessages' | 'resume' | 'spotifyCredentials' | 'testEcho' | 'timezone' | 'user' | 'viewer' | QueryKeySpecifier)[]; +export type QueryKeySpecifier = ('activityStatus' | 'announcement' | 'bootedAt' | 'contactEmail' | 'currentlyPlaying' | 'explorations' | 'googleCredentials' | 'icloudCredentials' | 'imageBySignedId' | 'instagramCredentials' | 'journalEntry' | 'journalEntryComments' | 'journeysSession' | 'location' | 'locationAccessGrants' | 'passwordStrength' | 'pensieveMessages' | 'resume' | 'spotifyCredentials' | 'testEcho' | 'timezone' | 'user' | 'viewer' | QueryKeySpecifier)[]; export type QueryFieldPolicy = { activityStatus?: FieldPolicy | FieldReadFunction, announcement?: FieldPolicy | FieldReadFunction, @@ -211,7 +211,7 @@ export type QueryFieldPolicy = { instagramCredentials?: FieldPolicy | FieldReadFunction, journalEntry?: FieldPolicy | FieldReadFunction, journalEntryComments?: FieldPolicy | FieldReadFunction, - journeySession?: FieldPolicy | FieldReadFunction, + journeysSession?: FieldPolicy | FieldReadFunction, location?: FieldPolicy | FieldReadFunction, locationAccessGrants?: FieldPolicy | FieldReadFunction, passwordStrength?: FieldPolicy | FieldReadFunction, @@ -288,11 +288,11 @@ export type SpotifyTrackFieldPolicy = { name?: FieldPolicy | FieldReadFunction, url?: FieldPolicy | FieldReadFunction }; -export type SubscriptionKeySpecifier = ('activityStatus' | 'currentlyPlaying' | 'journeySessionParticipation' | 'location' | 'pensieveMessage' | 'testSubscription' | SubscriptionKeySpecifier)[]; +export type SubscriptionKeySpecifier = ('activityStatus' | 'currentlyPlaying' | 'journeysSessionParticipation' | 'location' | 'pensieveMessage' | 'testSubscription' | SubscriptionKeySpecifier)[]; export type SubscriptionFieldPolicy = { activityStatus?: FieldPolicy | FieldReadFunction, currentlyPlaying?: FieldPolicy | FieldReadFunction, - journeySessionParticipation?: FieldPolicy | FieldReadFunction, + journeysSessionParticipation?: FieldPolicy | FieldReadFunction, location?: FieldPolicy | FieldReadFunction, pensieveMessage?: FieldPolicy | FieldReadFunction, testSubscription?: FieldPolicy | FieldReadFunction @@ -423,13 +423,13 @@ export type StrictTypedTypePolicies = { keyFields?: false | JournalEntryKeySpecifier | (() => undefined | JournalEntryKeySpecifier), fields?: JournalEntryFieldPolicy, }, - JourneySession?: Omit & { - keyFields?: false | JourneySessionKeySpecifier | (() => undefined | JourneySessionKeySpecifier), - fields?: JourneySessionFieldPolicy, + JourneysSession?: Omit & { + keyFields?: false | JourneysSessionKeySpecifier | (() => undefined | JourneysSessionKeySpecifier), + fields?: JourneysSessionFieldPolicy, }, - JourneySessionParticipation?: Omit & { - keyFields?: false | JourneySessionParticipationKeySpecifier | (() => undefined | JourneySessionParticipationKeySpecifier), - fields?: JourneySessionParticipationFieldPolicy, + JourneysSessionParticipation?: Omit & { + keyFields?: false | JourneysSessionParticipationKeySpecifier | (() => undefined | JourneysSessionParticipationKeySpecifier), + fields?: JourneysSessionParticipationFieldPolicy, }, LikePensieveMessagePayload?: Omit & { keyFields?: false | LikePensieveMessagePayloadKeySpecifier | (() => undefined | LikePensieveMessagePayloadKeySpecifier), diff --git a/app/helpers/apollo/introspection.generated.ts b/app/helpers/apollo/introspection.generated.ts index 649ddfa7..4e5137d6 100644 --- a/app/helpers/apollo/introspection.generated.ts +++ b/app/helpers/apollo/introspection.generated.ts @@ -11,8 +11,8 @@ "Image", "InstagramCredentials", "JournalEntry", - "JourneySession", - "JourneySessionParticipation", + "JourneysSession", + "JourneysSessionParticipation", "LocationAccessGrant", "LocationLog", "OAuthCredentials", diff --git a/app/helpers/graphql/operations.generated.ts b/app/helpers/graphql/operations.generated.ts index 58258be4..3a3b7311 100644 --- a/app/helpers/graphql/operations.generated.ts +++ b/app/helpers/graphql/operations.generated.ts @@ -318,10 +318,10 @@ export type JournalEntryEntryFragment = ( & Pick ); -export type JourneyHomePageQueryVariables = Types.Exact<{ [key: string]: never; }>; +export type JourneysHomePageQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type JourneyHomePageQuery = ( +export type JourneysHomePageQuery = ( { __typename?: 'Query' } & { viewer: Types.Maybe<( { __typename?: 'User' } @@ -329,19 +329,19 @@ export type JourneyHomePageQuery = ( )> } ); -export type JourneySessionPageQueryVariables = Types.Exact<{ +export type JourneysSessionPageQueryVariables = Types.Exact<{ sessionId: Types.Scalars['ID']['input']; }>; -export type JourneySessionPageQuery = ( +export type JourneysSessionPageQuery = ( { __typename?: 'Query' } & { session: Types.Maybe<( - { __typename?: 'JourneySession' } - & Pick + { __typename?: 'JourneysSession' } + & Pick & { participations: Array<( - { __typename?: 'JourneySessionParticipation' } - & Pick + { __typename?: 'JourneysSessionParticipation' } + & Pick )> } )>, viewer: Types.Maybe<( { __typename?: 'User' } @@ -349,16 +349,16 @@ export type JourneySessionPageQuery = ( )> } ); -export type JourneySessionPageSubscriptionVariables = Types.Exact<{ +export type JourneysSessionPageSubscriptionVariables = Types.Exact<{ sessionId: Types.Scalars['ID']['input']; }>; -export type JourneySessionPageSubscription = ( +export type JourneysSessionPageSubscription = ( { __typename?: 'Subscription' } & { participation: Types.Maybe<( - { __typename?: 'JourneySessionParticipation' } - & Pick + { __typename?: 'JourneysSessionParticipation' } + & Pick )> } ); @@ -1000,9 +1000,9 @@ export const HomePageQueryDocument = {"kind":"Document","definitions":[{"kind":" export const ImportJournalEntriesMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportJournalEntriesMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportJournalEntriesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"payload"},"name":{"kind":"Name","value":"importJournalEntries"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; export const ImportLocationLogsMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportLocationLogsMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportLocationLogsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"payload"},"name":{"kind":"Name","value":"importLocationLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; export const JournalEntryCommentsQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"JournalEntryCommentsQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"entryId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"comments"},"name":{"kind":"Name","value":"journalEntryComments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"entryId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"entryId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"richText"}}]}}]}}]} as unknown as DocumentNode; -export const JourneyHomePageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"JourneyHomePageQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AppViewerFragment"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AppViewerFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; -export const JourneySessionPageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"JourneySessionPageQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"session"},"name":{"kind":"Name","value":"journeySession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"participations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"goal"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"participantIsViewer"}},{"kind":"Field","name":{"kind":"Name","value":"participantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AppViewerFragment"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AppViewerFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; -export const JourneySessionPageSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"JourneySessionPageSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"participation"},"name":{"kind":"Name","value":"journeySessionParticipation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"sessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const JourneysHomePageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"JourneysHomePageQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AppViewerFragment"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AppViewerFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; +export const JourneysSessionPageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"JourneysSessionPageQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"session"},"name":{"kind":"Name","value":"journeysSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"participations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"goal"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"participantIsViewer"}},{"kind":"Field","name":{"kind":"Name","value":"participantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AppViewerFragment"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AppViewerFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; +export const JourneysSessionPageSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"JourneysSessionPageSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"participation"},"name":{"kind":"Name","value":"journeysSessionParticipation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"sessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const LikePensieveMessageMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LikePensieveMessageMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LikePensieveMessageInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"payload"},"name":{"kind":"Name","value":"likePensieveMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; export const LocatePageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LocatePageQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"location"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"approximateCoordinates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"latitude"}},{"kind":"Field","name":{"kind":"Name","value":"longitude"}}]}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AppViewerFragment"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AppViewerFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; export const LocatePageSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LocatePageSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"location"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"coordinates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"latitude"}},{"kind":"Field","name":{"kind":"Name","value":"longitude"}}]}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"trail"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LocatePageTrailMarkerFragment"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LocatePageTrailMarkerFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LocationTrailMarker"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"coordinates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"latitude"}},{"kind":"Field","name":{"kind":"Name","value":"longitude"}}]}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]} as unknown as DocumentNode; diff --git a/app/helpers/graphql/types.generated.ts b/app/helpers/graphql/types.generated.ts index d2d25d84..8a414ea1 100644 --- a/app/helpers/graphql/types.generated.ts +++ b/app/helpers/graphql/types.generated.ts @@ -204,23 +204,23 @@ export type JournalEntry = Node & { url: Scalars['String']['output']; }; -export type JourneySession = Node & { - __typename?: 'JourneySession'; +export type JourneysSession = Node & { + __typename?: 'JourneysSession'; /** ID of the object. */ id: Scalars['ID']['output']; - participations: Array; + participations: Array; startedAt: Scalars['DateTime']['output']; url: Scalars['String']['output']; }; -export type JourneySessionParticipation = Node & { - __typename?: 'JourneySessionParticipation'; +export type JourneysSessionParticipation = Node & { + __typename?: 'JourneysSessionParticipation'; goal: Scalars['String']['output']; /** ID of the object. */ id: Scalars['ID']['output']; participantIsViewer: Scalars['Boolean']['output']; participantName: Scalars['String']['output']; - session: Array; + session: Array; }; /** Autogenerated input type of LikePensieveMessage */ @@ -478,7 +478,7 @@ export type Query = { instagramCredentials?: Maybe; journalEntry?: Maybe; journalEntryComments: Array; - journeySession?: Maybe; + journeysSession?: Maybe; location?: Maybe; locationAccessGrants: Array; passwordStrength: Scalars['Float']['output']; @@ -507,7 +507,7 @@ export type QueryJournalEntryCommentsArgs = { }; -export type QueryJourneySessionArgs = { +export type QueryJourneysSessionArgs = { id: Scalars['ID']['input']; }; @@ -682,14 +682,14 @@ export type Subscription = { __typename?: 'Subscription'; activityStatus?: Maybe; currentlyPlaying?: Maybe; - journeySessionParticipation?: Maybe; + journeysSessionParticipation?: Maybe; location?: Maybe; pensieveMessage?: Maybe; testSubscription: Scalars['Int']['output']; }; -export type SubscriptionJourneySessionParticipationArgs = { +export type SubscriptionJourneysSessionParticipationArgs = { sessionId: Scalars['ID']['input']; }; diff --git a/app/models/journey.rb b/app/models/journeys.rb similarity index 80% rename from app/models/journey.rb rename to app/models/journeys.rb index 95dca2e6..a2f7a963 100644 --- a/app/models/journey.rb +++ b/app/models/journeys.rb @@ -1,11 +1,11 @@ # typed: strict # frozen_string_literal: true -module Journey +module Journeys extend T::Sig sig { returns(String) } def self.table_name_prefix - "journey_" + "journeys_" end end diff --git a/app/models/journey/session.rb b/app/models/journeys/session.rb similarity index 90% rename from app/models/journey/session.rb rename to app/models/journeys/session.rb index e2af350b..14985781 100644 --- a/app/models/journey/session.rb +++ b/app/models/journeys/session.rb @@ -3,7 +3,7 @@ # == Schema Information # -# Table name: journey_sessions +# Table name: journeys_sessions # # id :uuid not null, primary key # slug :string not null @@ -12,9 +12,9 @@ # # Indexes # -# index_journey_sessions_on_slug (slug) UNIQUE +# index_journeys_sessions_on_slug (slug) UNIQUE # -module Journey +module Journeys class Session < ApplicationRecord extend FriendlyId include Identifiable diff --git a/app/models/journey/session_participation.rb b/app/models/journeys/session_participation.rb similarity index 82% rename from app/models/journey/session_participation.rb rename to app/models/journeys/session_participation.rb index 44e6e16a..24271d4c 100644 --- a/app/models/journey/session_participation.rb +++ b/app/models/journeys/session_participation.rb @@ -3,7 +3,7 @@ # == Schema Information # -# Table name: journey_session_participations +# Table name: journeys_session_participations # # id :uuid not null, primary key # goal :text not null @@ -15,18 +15,18 @@ # # Indexes # -# index_journey_session_participations_on_session_id (session_id) +# index_journeys_session_participations_on_session_id (session_id) # # Foreign Keys # -# fk_rails_... (session_id => journey_sessions.id) +# fk_rails_... (session_id => journeys_sessions.id) # -module Journey +module Journeys class SessionParticipation < ApplicationRecord # == Associations belongs_to :session, inverse_of: :participations - sig { returns(Journey::Session) } + sig { returns(Session) } def session! session or raise ActiveRecord::RecordNotFound, "missing session" end diff --git a/app/pages/JourneyHomePage.tsx b/app/pages/JourneysHomePage.tsx similarity index 64% rename from app/pages/JourneyHomePage.tsx rename to app/pages/JourneysHomePage.tsx index 36381e26..b3949bec 100644 --- a/app/pages/JourneyHomePage.tsx +++ b/app/pages/JourneysHomePage.tsx @@ -1,13 +1,14 @@ import type { PageComponent, PagePropsWithData } from "~/helpers/inertia"; import { Text } from "@mantine/core"; import { useAudioRecorder } from "react-audio-voice-recorder"; +import MicIcon from "~icons/heroicons/microphone-20-solid"; -import type { JourneyHomePageQuery } from "~/helpers/graphql"; +import type { JourneysHomePageQuery } from "~/helpers/graphql"; -import JourneyAppLayout from "~/components/JourneyAppLayout"; +import JourneysAppLayout from "~/components/JourneysAppLayout"; import { randomAnimal } from "~/helpers/animals"; -export type JourneyHomePageProps = PagePropsWithData; +export type JourneyHomePageProps = PagePropsWithData; const JourneyHomePage: PageComponent = () => { const router = useRouter(); @@ -37,13 +38,14 @@ const JourneyHomePage: PageComponent = () => { return ( - watchya gonna do for the next hour? + watchya gonna do for the next hour? + + + + we'll start a 1-hour timer for you to do ur thing. +
+ send this link to other ppl so they can join your session, to keep u + motivated & held accountable. +
+
+
); }; JourneyHomePage.layout = buildLayout( (page, { data: { viewer } }) => ( - + {page} - + ), ); diff --git a/app/pages/JourneySessionPage.module.css b/app/pages/JourneysSessionPage.module.css similarity index 100% rename from app/pages/JourneySessionPage.module.css rename to app/pages/JourneysSessionPage.module.css diff --git a/app/pages/JourneySessionPage.tsx b/app/pages/JourneysSessionPage.tsx similarity index 56% rename from app/pages/JourneySessionPage.tsx rename to app/pages/JourneysSessionPage.tsx index 9ae1ec55..86fa9663 100644 --- a/app/pages/JourneySessionPage.tsx +++ b/app/pages/JourneysSessionPage.tsx @@ -1,21 +1,22 @@ import type { PageComponent, PagePropsWithData } from "~/helpers/inertia"; -import { RingProgress, Text } from "@mantine/core"; +import { ActionIcon, CopyButton, RingProgress, Text } from "@mantine/core"; -import { - JourneySessionPageSubscriptionDocument, - type JourneySessionPageQuery, -} from "~/helpers/graphql"; +import { JourneysSessionPageSubscriptionDocument } from "~/helpers/graphql"; +import type { JourneysSessionPageQuery } from "~/helpers/graphql"; -import JourneyAppLayout from "~/components/JourneyAppLayout"; +import JourneysAppLayout from "~/components/JourneysAppLayout"; -import classes from "./JourneySessionPage.module.css"; +import classes from "./JourneysSessionPage.module.css"; -export type JourneySessionPageProps = - PagePropsWithData; +export type JourneysSessionPageProps = + PagePropsWithData & { + readonly homepageUrl: string; + }; const MAX_COUNTDOWN_SECONDS = 3600; -const JourneySessionPage: PageComponent = ({ +const JourneySessionPage: PageComponent = ({ + homepageUrl, data: { session }, }) => { invariant(session, "Missing session"); @@ -52,7 +53,7 @@ const JourneySessionPage: PageComponent = ({ }, [countdownSeconds]); // == Participation - useSubscription(JourneySessionPageSubscriptionDocument, { + useSubscription(JourneysSessionPageSubscriptionDocument, { onData: ({ data: { data } }) => { if (data) { router.reload({ preserveScroll: true }); @@ -66,9 +67,18 @@ const JourneySessionPage: PageComponent = ({ }, }); + // == Sharing + const sharingText = useMemo(() => { + return ( + `heyo, i'm using ${homepageUrl} to be intentional about my time. ` + + `if u join in the next couple of minutes, you can be a part of my ` + + `session :)` + ); + }, [homepageUrl]); + return ( - go go go! + go go go! u got thisss = ({ thickness={7} roundCaps /> + + + + Know someone who wants to be more intentional about how they spend + their time? send them a text: + + +