├── .node-version
├── priv
├── repo
│ └── migrations
│ │ ├── .gitkeep
│ │ ├── .formatter.exs
│ │ ├── 20171128013421_add_color_to_paces.exs
│ │ ├── 20190402033557_add_content_to_races.exs
│ │ ├── 20190408212922_add_type_to_activities.exs
│ │ ├── 20190709235126_add_logo_url_to_races.exs
│ │ ├── 20200605032415_add_start_date_to_races.exs
│ │ ├── 20200603042523_add_course_url_to_races.exs
│ │ ├── 20210118224433_add_amount_to_scores.exs
│ │ ├── 20190208052014_add_password_to_users.exs
│ │ ├── 20190930021245_add_moving_to_trackpoints.exs
│ │ ├── 20180830035119_add_polyline_to_activities.exs
│ │ ├── 20181218045915_add_timezone_to_user_prefs.exs
│ │ ├── 20190717045528_add_type_to_training_events.exs
│ │ ├── 20210118231417_add_polyline_to_challenges.exs
│ │ ├── 20180813025524_add_experience_to_user_prefs.exs
│ │ ├── 20180819213812_add_sync_at_to_credentials.exs
│ │ ├── 20190125152555_add_stripe_customer_to_users.exs
│ │ ├── 20190514193155_add_billing_indexes_to_users.exs
│ │ ├── 20190709235333_add_registration_url_to_races.exs
│ │ ├── 20190815044116_add_description_to_activities.exs
│ │ ├── 20210118215927_add_segment_id_to_challenges.exs
│ │ ├── 20211208042552_add_description_to_challenges.exs
│ │ ├── 20191003033815_add_workout_type_to_activities.exs
│ │ ├── 20200606041837_add_geo_to_races.exs
│ │ ├── 20190827194518_add_elevation_gain_to_activities.exs
│ │ ├── 20210103165448_add_activity_type_to_activities.exs
│ │ ├── 20190103225546_add_status_to_activities.exs
│ │ ├── 20190209193444_add_registered_to_users.exs
│ │ ├── 20220802163013_add_personal_records_to_user_prefs.exs
│ │ ├── 20181224172858_add_imperial_to_user_prefs.exs
│ │ ├── 20190323172949_add_start_at_to_races.exs
│ │ ├── 20191023024347_add_token_secret_to_credentials.exs
│ │ ├── 20210304045021_add_start_at_local_to_activities.exs
│ │ ├── 20190102150636_add_complete_to_activities.exs
│ │ ├── 20200528035410_add_qualifier_to_races.exs
│ │ ├── 20210630031806_add_recurring_to_challenges.exs
│ │ ├── 20211026193306_add_trackpoints_json_to_trackpoint_sets.exs
│ │ ├── 20190223001437_add_default_to_billing_plans.exs
│ │ ├── 20190831205355_add_gender_bday_to_user_prefs.exs
│ │ ├── 20230105050138_add_api_enabled_to_user_prefs.exs
│ │ ├── 20190416033230_add_trial_end_to_users.exs
│ │ ├── 20190307045323_create_subscriptions.exs
│ │ ├── 20220801163056_add_training_information_to_race_goals.exs
│ │ ├── 20190211040437_add_payment_processor_fields_to_users.exs
│ │ ├── 20190805033850_add_distance_unit_to_activities.exs
│ │ ├── 20190323040158_add_breadcrumbs_to_races.exs
│ │ ├── 20190323162043_add_address_to_races.exs
│ │ ├── 20190825032057_add_distance_amount_to_activities.exs
│ │ ├── 20210304040854_add_slug_to_users.exs
│ │ ├── 20171128012003_add_email_to_users.exs
│ │ ├── 20181118233957_add_refresh_token_to_credentials.exs
│ │ ├── 20180910032145_add_event_id_to_activities.exs
│ │ ├── 20210729185425_add_follow_count_to_users.exs
│ │ ├── 20190223223014_create_webhook_events.exs
│ │ ├── 20200916033210_change_score_to_float.exs
│ │ ├── 20220727223626_add_slug_to_race_goals.exs
│ │ ├── 20221224224446_add_slug_to_activities.exs
│ │ ├── 20181223185441_add_planning_fields_to_activities.exs
│ │ ├── 20190203082729_change_uid_to_string.exs
│ │ ├── 20181201175430_change_polyline_to_text.exs
│ │ ├── 20190628000419_add_external_id_to_races.exs
│ │ ├── 20200529044028_add_course_profile_to_races.exs
│ │ ├── 20180817041247_change_distance_to_integer.exs
│ │ ├── 20210212233646_change_challenge_polyline_type.exs
│ │ ├── 20190223235513_change_external_id_to_string.exs
│ │ ├── 20190122140903_add_basic_fields_to_training_plans.exs
│ │ ├── 20190501142943_add_trackpoints_belong_to_set.exs
│ │ ├── 20171109012545_add_external_id_to_activities.exs
│ │ ├── 20190501142700_create_trackpoint_sets.exs
│ │ ├── 20171105203801_create_paces.exs
│ │ ├── 20190121231335_create_training_plans.exs
│ │ ├── 20220301173633_add_namer_fields_to_user_prefs.exs
│ │ ├── 20200916034108_add_share_fields_to_challenges.exs
│ │ ├── 20181223192749_change_external_id_for_activities.exs
│ │ ├── 20190218002113_create_billing_plans.exs
│ │ ├── 20210217044932_add_dates_to_challenges.exs
│ │ ├── 20220924032609_add_activity_index_to_trackpoint_sets.exs
│ │ ├── 20171022220118_create_users.exs
│ │ ├── 20190324012558_create_race_trackpoints.exs
│ │ ├── 20210123063435_create_push_tokens.exs
│ │ ├── 20180819194130_remove_paces_from_events.exs
│ │ ├── 20171105204828_create_activities.exs
│ │ ├── 20180806045623_create_user_prefs.exs
│ │ ├── 20190318033427_create_races.exs
│ │ ├── 20171022220412_create_credentials.exs
│ │ ├── 20171105203451_create_goals.exs
│ │ ├── 20190501143151_remote_activity_from_trackpoints.exs
│ │ ├── 20190423031111_create_billing_invoices.exs
│ │ ├── 20190627171045_create_race_events.exs
│ │ ├── 20181223194415_add_default_fields_to_activities.exs
│ │ ├── 20211106190100_add_default_timeline_to_challenges.exs
│ │ ├── 20190425025635_add_default_subscription_status.exs
│ │ ├── 20190102231358_add_default_timezone_user_prefs.exs
│ │ ├── 20210307043053_create_challenge_activities.exs
│ │ ├── 20190224020703_create_trackpoints.exs
│ │ ├── 20180817035931_change_user_prefs_reference_to_users.exs
│ │ ├── 20211216185228_create_race_goals.exs
│ │ ├── 20200907210117_create_challenges.exs
│ │ ├── 20200912221618_create_scores.exs
│ │ ├── 20210729181828_create_follows.exs
│ │ ├── 20211229174335_default_to_free_subscription.exs
│ │ ├── 20200912194830_create_user_challenge.exs
│ │ ├── 20171105204155_create_events.exs
│ │ ├── 20200708041417_create_result_summaries.exs
│ │ └── 20190715014941_create_training_events.exs
└── static
│ ├── favicon.ico
│ ├── fonts
│ ├── icons.eot
│ ├── icons.otf
│ ├── icons.ttf
│ ├── digital.ttf
│ ├── icons.woff
│ └── icons.woff2
│ ├── images
│ ├── flags.png
│ ├── flags.webp
│ ├── strava.png
│ ├── favicon.png
│ ├── logo@2x.png
│ ├── bg-terrain.jpg
│ ├── btn_strava@2x.png
│ ├── home
│ │ ├── dashboard.jpg
│ │ ├── runner@1x.jpg
│ │ ├── runner@2x.jpg
│ │ ├── calendar@1x.jpg
│ │ ├── calendar@2x.jpg
│ │ ├── dashboard.webp
│ │ ├── goal-shape.png
│ │ ├── heart-shape.png
│ │ ├── overview@1x.jpg
│ │ ├── runner@1x.webp
│ │ ├── runner@2x.webp
│ │ ├── track-shape.png
│ │ ├── calendar@1x.webp
│ │ ├── calendar@2x.webp
│ │ ├── dashboard@1x.jpg
│ │ ├── dashboard@1x.webp
│ │ ├── dashboard@2x.jpg
│ │ └── dashboard@2x.webp
│ ├── logo-white@2x.png
│ └── namer-example.jpg
│ └── robots.txt
├── assets
├── css
│ ├── core
│ │ ├── medias
│ │ │ ├── _media.scss
│ │ │ └── _media-comment.scss
│ │ ├── headers
│ │ │ └── _header.scss
│ │ ├── utilities
│ │ │ ├── _sizing.scss
│ │ │ ├── _image.scss
│ │ │ ├── _overflow.scss
│ │ │ ├── _helper.scss
│ │ │ ├── _transform.scss
│ │ │ ├── _position.scss
│ │ │ ├── _shadows.scss
│ │ │ └── _opacity.scss
│ │ ├── maps
│ │ │ └── _map.scss
│ │ ├── reboot
│ │ │ └── _reboot.scss
│ │ ├── type
│ │ │ ├── _display.scss
│ │ │ ├── _article.scss
│ │ │ └── _type.scss
│ │ ├── cards
│ │ │ ├── _card-animations.scss
│ │ │ ├── _card-stats.scss
│ │ │ ├── _card-blockquote.scss
│ │ │ ├── _card-pricing.scss
│ │ │ └── _card-money.scss
│ │ ├── buttons
│ │ │ └── _button-brand.scss
│ │ ├── custom-forms
│ │ │ ├── _custom-select.scss
│ │ │ └── _custom-checkbox.scss
│ │ ├── mixins
│ │ │ ├── _icon.scss
│ │ │ ├── _badge.scss
│ │ │ ├── _modals.scss
│ │ │ ├── _alert.scss
│ │ │ └── _popover.scss
│ │ ├── avatars
│ │ │ └── _avatar-group.scss
│ │ ├── navbars
│ │ │ ├── _navbar-floating.scss
│ │ │ └── _navbar-collapse.scss
│ │ ├── popovers
│ │ │ └── _popover.scss
│ │ ├── badges
│ │ │ ├── _badge-floating.scss
│ │ │ └── _badge-circle.scss
│ │ ├── vendors
│ │ │ ├── _sweet-alert-2.scss
│ │ │ ├── _chartjs.scss
│ │ │ ├── _headroom.scss
│ │ │ ├── _jvectormap.scss
│ │ │ └── _datatables.scss
│ │ ├── tables
│ │ │ └── _table-actions.scss
│ │ ├── content
│ │ │ └── _main-content.scss
│ │ ├── modals
│ │ │ └── _modal.scss
│ │ ├── shortcuts
│ │ │ └── _shortcut.scss
│ │ ├── masks
│ │ │ └── _mask.scss
│ │ ├── navs
│ │ │ └── _nav.scss
│ │ ├── grid
│ │ │ └── _grid.scss
│ │ ├── collapse
│ │ │ └── _accordion.scss
│ │ └── icons
│ │ │ └── _icon-shape.scss
│ ├── custom
│ │ ├── _mixins.scss
│ │ ├── _utilities.scss
│ │ └── _functions.scss
│ └── pages
│ │ └── _namer.scss
├── js
│ ├── hooks
│ │ ├── slim-select.js
│ │ ├── modal.js
│ │ └── base.js
│ ├── components
│ │ ├── modal.js
│ │ ├── timezone-hidden-input.js
│ │ ├── base.js
│ │ ├── imperial-hidden-input.js
│ │ ├── alert.js
│ │ ├── avatar.js
│ │ ├── date-picker.js
│ │ ├── btn-spinner.js
│ │ ├── copy-input.js
│ │ ├── sign-up-form.js
│ │ └── distance-select.js
│ ├── dashboard.js
│ ├── bootstrap.js
│ ├── fonts.js
│ └── app.js
├── static
│ └── .well-known
│ │ ├── apple-app-site-association
│ │ └── assetlinks.json
└── webp.js
├── .tool-versions
├── Procfile
├── lib
├── squeeze.ex
├── squeeze_web
│ ├── templates
│ │ ├── layout
│ │ │ ├── email.text.eex
│ │ │ ├── app.html.eex
│ │ │ └── live.html.heex
│ │ ├── shared
│ │ │ ├── waves.html.eex
│ │ │ ├── avatar.html.eex
│ │ │ ├── _gtm.html.eex
│ │ │ └── flash.html.eex
│ │ ├── home
│ │ │ ├── namer.html.eex
│ │ │ ├── index.html.eex
│ │ │ ├── form.html.eex
│ │ │ ├── _dashboard.html.eex
│ │ │ └── _schema.html.eex
│ │ ├── map
│ │ │ ├── route-map.html.eex
│ │ │ └── activity-map.html.eex
│ │ ├── race
│ │ │ ├── _address.html.eex
│ │ │ ├── _faq.html.eex
│ │ │ ├── _breadcrumbs.html.eex
│ │ │ ├── _hero.html.eex
│ │ │ ├── _events.html.eex
│ │ │ └── _schema.html.eex
│ │ ├── email
│ │ │ ├── welcome.text.eex
│ │ │ └── reset_password.text.eex
│ │ ├── menu
│ │ │ └── _avatar.html.eex
│ │ ├── sitemap
│ │ │ └── index.xml.eex
│ │ ├── page
│ │ │ └── support.html.eex
│ │ ├── activity_chart
│ │ │ └── chart.html.eex
│ │ ├── forgot_password
│ │ │ └── form.html.eex
│ │ ├── modal
│ │ │ └── past-due.html.eex
│ │ └── reset_password
│ │ │ └── form.html.eex
│ ├── views
│ │ ├── auth_view.ex
│ │ ├── email_view.ex
│ │ ├── home_view.ex
│ │ ├── layout_view.ex
│ │ ├── session_view.ex
│ │ ├── shared_view.ex
│ │ ├── sitemap_view.ex
│ │ ├── challenge_view.ex
│ │ ├── forgot_password_view.ex
│ │ ├── page_view.ex
│ │ ├── fitbit_webhook_view.ex
│ │ ├── garmin_webhook_view.ex
│ │ ├── stripe_webhook_view.ex
│ │ ├── menu_view.ex
│ │ ├── reset_password_view.ex
│ │ ├── api
│ │ │ ├── google_auth_view.ex
│ │ │ ├── push_token_view.ex
│ │ │ ├── strava_view.ex
│ │ │ ├── user_prefs_view.ex
│ │ │ ├── follow_view.ex
│ │ │ ├── error_view.ex
│ │ │ ├── changeset_view.ex
│ │ │ └── challenge_activity_view.ex
│ │ ├── user_view.ex
│ │ ├── search_view.ex
│ │ ├── strava_webhook_view.ex
│ │ ├── region_search_view.ex
│ │ ├── error_view.ex
│ │ ├── modal_view.ex
│ │ ├── honeypot_input.ex
│ │ ├── challenge_share_view.ex
│ │ └── distance_search_view.ex
│ ├── live
│ │ ├── navbar_component.ex
│ │ ├── settings
│ │ │ ├── api_config_component.ex
│ │ │ ├── user_form_component.ex
│ │ │ └── namer_card_component.ex
│ │ ├── dashboard
│ │ │ ├── challenges_card_component.ex
│ │ │ ├── mini_activity_card_component.ex
│ │ │ ├── recent_activities_card_component.ex
│ │ │ ├── load_history_component.ex
│ │ │ ├── load_history_component.html.heex
│ │ │ └── mini_calendar_component.ex
│ │ ├── flash_component.ex
│ │ ├── avatar_component.html.heex
│ │ ├── weekly_summary_component.html.heex
│ │ ├── svg_polyline_component.html.heex
│ │ ├── activities
│ │ │ ├── chart_component.html.heex
│ │ │ └── map_component.html.heex
│ │ ├── challenges
│ │ │ ├── static_map_component.html.heex
│ │ │ ├── show_live.ex
│ │ │ ├── static_map_component.ex
│ │ │ └── podium_item_component.html.heex
│ │ ├── weekly_summary_component.ex
│ │ ├── challenge_live.ex
│ │ ├── live_auth.ex
│ │ └── race_live
│ │ │ └── upcoming_races_card.ex
│ ├── controllers
│ │ ├── sitemap_controller.ex
│ │ ├── dashboard_controller.ex
│ │ ├── challenge_share_controller.ex
│ │ ├── search_controller.ex
│ │ ├── api
│ │ │ ├── challenge_activity_controller.ex
│ │ │ ├── user_prefs_controller.ex
│ │ │ └── push_token_controller.ex
│ │ ├── garmin_webhook_controller.ex
│ │ ├── page_controller.ex
│ │ ├── distance_search_controller.ex
│ │ ├── region_search_controller.ex
│ │ └── challenge_controller.ex
│ ├── plugs
│ │ └── require_registered.ex
│ └── gettext.ex
├── squeeze
│ ├── mailer.ex
│ ├── scheduler.ex
│ ├── api
│ │ ├── auth_error_handler.ex
│ │ └── auth_pipeline.ex
│ ├── races
│ │ ├── trackpoint.ex
│ │ ├── event.ex
│ │ └── result_summary.ex
│ ├── company_helper.ex
│ ├── dashboard
│ │ ├── trackpoint.ex
│ │ └── trackpoint_set.ex
│ ├── slug_generator.ex
│ ├── auth_pipeline.ex
│ ├── live_auth_pipeline.ex
│ ├── mailing_list
│ │ └── subscription.ex
│ ├── garmin
│ │ ├── client.ex
│ │ └── middleware
│ │ │ └── oauth.ex
│ ├── logger
│ │ ├── webhook_event.ex
│ │ └── logger.ex
│ ├── namer
│ │ ├── duration_formatter.ex
│ │ └── relative_time_formatter.ex
│ ├── notifications
│ │ └── push_token.ex
│ ├── training_plans
│ │ └── event.ex
│ ├── payment_processor
│ │ └── payment_processor.ex
│ ├── race_search.ex
│ ├── strava
│ │ ├── activities.ex
│ │ └── client.ex
│ ├── billing
│ │ ├── plan.ex
│ │ └── invoice.ex
│ ├── reporter.ex
│ ├── release.ex
│ ├── challenges
│ │ ├── score.ex
│ │ └── challenge_activity.ex
│ ├── fitbit
│ │ └── history_loader.ex
│ ├── social
│ │ └── follow.ex
│ ├── email.ex
│ ├── stringable.ex
│ └── utils.ex
├── stripe
│ └── card_behavior.ex
├── strava
│ ├── auth_behavior.ex
│ ├── client_behavior.ex
│ ├── streams_behavior.ex
│ └── activities_behavior.ex
├── cache_body_reader.ex
└── mix
│ └── tasks
│ ├── add_slugs_to_activities.ex
│ ├── update_start_dates.ex
│ └── setup.stripe.ex
├── compile
├── phoenix_static_buildpack.config
├── rel
└── overlays
│ └── bin
│ ├── migrate.bat
│ ├── server.bat
│ ├── server
│ └── migrate
├── .github
├── imgs
│ ├── dashboard.png
│ └── dashboard-original.png
└── workflows
│ └── deploy.yml
├── yarn.lock
├── test
├── squeeze
│ ├── stats_test.exs
│ ├── oauth2
│ │ └── google_test.exs
│ ├── races
│ │ └── races_test.exs
│ ├── company_helper_test.exs
│ ├── duration_test.exs
│ ├── utils_test.exs
│ ├── email_test.exs
│ ├── billing
│ │ ├── invoice_test.exs
│ │ └── payment_method_test.exs
│ ├── accounts
│ │ ├── user_prefs_test.exs
│ │ └── credential_test.exs
│ ├── velocity_test.exs
│ ├── logger
│ │ └── logger_test.exs
│ ├── mailing_list
│ │ └── mailing_list_test.exs
│ └── challenges
│ │ └── score_updater_test.exs
├── squeeze_web
│ ├── views
│ │ ├── layout_view_test.exs
│ │ └── error_view_test.exs
│ ├── controllers
│ │ ├── billing_controller_test.exs
│ │ ├── race_controller_test.exs
│ │ ├── dashboard_controller_test.exs
│ │ ├── sitemap_controller_test.exs
│ │ ├── auth_controller_test.exs
│ │ ├── garmin_webhook_controller_test.exs
│ │ ├── fitbit_webhook_controller_test.exs
│ │ └── challenge_share_controller_test.exs
│ └── plugs
│ │ └── auth_test.exs
├── test_helper.exs
├── factories
│ ├── follow_factory.ex
│ ├── score_factory.ex
│ ├── push_token_factory.ex
│ ├── training_plan_factory.ex
│ ├── billing_plan_factory.ex
│ ├── invoice_factory.ex
│ ├── race_event_factory.ex
│ ├── detailed_activity_factory.ex
│ └── payment_method_factory.ex
└── support
│ ├── mocks.ex
│ └── factory.ex
├── .formatter.exs
└── coveralls.json
/.node-version:
--------------------------------------------------------------------------------
1 | 12.16.2
2 |
--------------------------------------------------------------------------------
/priv/repo/migrations/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/css/core/medias/_media.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Media
3 | //
4 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | erlang 24.3
2 | elixir 1.14.1-otp-24
3 | nodejs 18.7.0
4 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: MIX_ENV=prod elixir --sname server -S mix phx.server
2 |
--------------------------------------------------------------------------------
/lib/squeeze.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze do
2 | @moduledoc false
3 | end
4 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/layout/email.text.eex:
--------------------------------------------------------------------------------
1 | <%= @inner_content %>
2 |
--------------------------------------------------------------------------------
/compile:
--------------------------------------------------------------------------------
1 | yarn deploy
2 | cd $phoenix_dir
3 | mix "${phoenix_ex}.digest"
4 |
--------------------------------------------------------------------------------
/phoenix_static_buildpack.config:
--------------------------------------------------------------------------------
1 | node_version=12.16.2
2 | compile="compile"
3 |
--------------------------------------------------------------------------------
/rel/overlays/bin/migrate.bat:
--------------------------------------------------------------------------------
1 | call "%~dp0\squeeze" eval Squeeze.Release.migrate
2 |
--------------------------------------------------------------------------------
/rel/overlays/bin/server.bat:
--------------------------------------------------------------------------------
1 | set PHX_SERVER=true
2 | call "%~dp0\squeeze" start
3 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/favicon.ico
--------------------------------------------------------------------------------
/.github/imgs/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/.github/imgs/dashboard.png
--------------------------------------------------------------------------------
/priv/repo/migrations/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/priv/static/fonts/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/fonts/icons.eot
--------------------------------------------------------------------------------
/priv/static/fonts/icons.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/fonts/icons.otf
--------------------------------------------------------------------------------
/priv/static/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/fonts/icons.ttf
--------------------------------------------------------------------------------
/priv/static/fonts/digital.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/fonts/digital.ttf
--------------------------------------------------------------------------------
/priv/static/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/fonts/icons.woff
--------------------------------------------------------------------------------
/priv/static/fonts/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/fonts/icons.woff2
--------------------------------------------------------------------------------
/priv/static/images/flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/flags.png
--------------------------------------------------------------------------------
/priv/static/images/flags.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/flags.webp
--------------------------------------------------------------------------------
/priv/static/images/strava.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/strava.png
--------------------------------------------------------------------------------
/priv/static/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/favicon.png
--------------------------------------------------------------------------------
/priv/static/images/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/logo@2x.png
--------------------------------------------------------------------------------
/rel/overlays/bin/server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd -P -- "$(dirname -- "$0")"
3 | PHX_SERVER=true exec ./squeeze start
4 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/css/core/headers/_header.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Header
3 | //
4 |
5 | .header {
6 | position: relative;
7 | }
8 |
--------------------------------------------------------------------------------
/priv/static/images/bg-terrain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/bg-terrain.jpg
--------------------------------------------------------------------------------
/.github/imgs/dashboard-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/.github/imgs/dashboard-original.png
--------------------------------------------------------------------------------
/assets/css/core/utilities/_sizing.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Height
3 | //
4 |
5 | .h-100vh {
6 | height: 100vh !important;
7 | }
8 |
--------------------------------------------------------------------------------
/priv/static/images/btn_strava@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/btn_strava@2x.png
--------------------------------------------------------------------------------
/priv/static/images/home/dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/dashboard.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/runner@1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/runner@1x.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/runner@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/runner@2x.jpg
--------------------------------------------------------------------------------
/priv/static/images/logo-white@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/logo-white@2x.png
--------------------------------------------------------------------------------
/priv/static/images/namer-example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/namer-example.jpg
--------------------------------------------------------------------------------
/rel/overlays/bin/migrate:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd -P -- "$(dirname -- "$0")"
3 | exec ./squeeze eval Squeeze.Release.migrate
4 |
--------------------------------------------------------------------------------
/priv/static/images/home/calendar@1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/calendar@1x.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/calendar@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/calendar@2x.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/dashboard.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/dashboard.webp
--------------------------------------------------------------------------------
/priv/static/images/home/goal-shape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/goal-shape.png
--------------------------------------------------------------------------------
/priv/static/images/home/heart-shape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/heart-shape.png
--------------------------------------------------------------------------------
/priv/static/images/home/overview@1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/overview@1x.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/runner@1x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/runner@1x.webp
--------------------------------------------------------------------------------
/priv/static/images/home/runner@2x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/runner@2x.webp
--------------------------------------------------------------------------------
/priv/static/images/home/track-shape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/track-shape.png
--------------------------------------------------------------------------------
/test/squeeze/stats_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.StatsTest do
2 | use Squeeze.DataCase
3 |
4 | @moduledoc false
5 | end
6 |
--------------------------------------------------------------------------------
/lib/squeeze/mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Mailer do
2 | use Bamboo.Mailer, otp_app: :squeeze
3 |
4 | @moduledoc false
5 | end
6 |
--------------------------------------------------------------------------------
/lib/squeeze/scheduler.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Scheduler do
2 | use Quantum, otp_app: :squeeze
3 |
4 | @moduledoc false
5 | end
6 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/shared/waves.html.eex:
--------------------------------------------------------------------------------
1 |
" />
2 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/auth_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.AuthView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/email_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.EmailView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/home_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.HomeView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/priv/static/images/home/calendar@1x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/calendar@1x.webp
--------------------------------------------------------------------------------
/priv/static/images/home/calendar@2x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/calendar@2x.webp
--------------------------------------------------------------------------------
/priv/static/images/home/dashboard@1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/dashboard@1x.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/dashboard@1x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/dashboard@1x.webp
--------------------------------------------------------------------------------
/priv/static/images/home/dashboard@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/dashboard@2x.jpg
--------------------------------------------------------------------------------
/priv/static/images/home/dashboard@2x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/openpace/main/priv/static/images/home/dashboard@2x.webp
--------------------------------------------------------------------------------
/lib/squeeze_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.LayoutView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/session_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SessionView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/shared_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SharedView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/sitemap_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SitemapView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/test/squeeze_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.LayoutViewTest do
2 | use SqueezeWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/challenge_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ChallengeView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/billing_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.BillingControllerTest do
2 | use SqueezeWeb.ConnCase
3 | end
4 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/navbar_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.NavbarComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/forgot_password_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ForgotPasswordView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/home/namer.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= render "namer-hero.html", assigns %>
3 | <%= render "namer-faq.html", assigns %>
4 |
5 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | {:ok, _} = Application.ensure_all_started(:ex_machina)
2 |
3 | ExUnit.start()
4 |
5 | Ecto.Adapters.SQL.Sandbox.mode(Squeeze.Repo, :manual)
6 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/settings/api_config_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Settings.ApiConfigComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_image.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Image
3 | //
4 |
5 | .img-center {
6 | display: block;
7 | margin-left: auto;
8 | margin-right: auto;
9 | }
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/map/route-map.html.eex:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/race_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.RaceControllerTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | describe "#show" do
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto, :phoenix],
3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
4 | subdirectories: ["priv/*/migrations"]
5 | ]
6 |
--------------------------------------------------------------------------------
/assets/js/hooks/slim-select.js:
--------------------------------------------------------------------------------
1 | import SlimSelect from 'slim-select';
2 |
3 | export default {
4 | mounted() {
5 | this.slimSelect = new SlimSelect({ select: this.el });
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/dashboard/challenges_card_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Dashboard.ChallengesCardComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/dashboard/mini_activity_card_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Dashboard.MiniActivityCardComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.PageView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def title(_, _), do: gettext("Run Your Best Race Ever")
6 | end
7 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/dashboard/recent_activities_card_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Dashboard.RecentActivitiesCardComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 | end
5 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/fitbit_webhook_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.FitbitWebhookView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render(_, _) do
6 | %{}
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/garmin_webhook_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.GarminWebhookView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render(_, _) do
6 | %{}
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/stripe_webhook_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.StripeWebhookView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render(_, _) do
6 | %{}
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/assets/css/core/maps/_map.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Map
3 | //
4 |
5 | .map-canvas {
6 | position: relative;
7 | width: 100%;
8 | height: $map-height;
9 | border-radius: $border-radius;
10 | }
11 |
--------------------------------------------------------------------------------
/assets/css/core/reboot/_reboot.scss:
--------------------------------------------------------------------------------
1 | iframe {
2 | border: 0;
3 | }
4 |
5 | figcaption,
6 | figure,
7 | main {
8 | display: block;
9 | }
10 |
11 | main {
12 | overflow: hidden;
13 | }
14 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/menu_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.MenuView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def in_trial?(user) do
6 | user.subscription_status == :trialing
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_overflow.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Overflow
3 | //
4 |
5 | .overflow-visible {
6 | overflow: visible !important;
7 | }
8 |
9 | .overflow-hidden {
10 | overflow: hidden !important;
11 | }
12 |
--------------------------------------------------------------------------------
/assets/js/hooks/modal.js:
--------------------------------------------------------------------------------
1 | export default {
2 | mounted() {
3 | window.$(this.el).modal('show');
4 | this.el.addEventListener('hide-modal', () => {
5 | window.$(this.el).modal('hide');
6 | });
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/reset_password_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ResetPasswordView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def title(_page, _assigns) do
6 | "Reset Password"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/assets/css/core/type/_display.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Display
3 | //
4 |
5 |
6 | .display-1,
7 | .display-2,
8 | .display-3,
9 | .display-4 {
10 | span {
11 | display: block;
12 | font-weight: $font-weight-light;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/assets/js/components/modal.js:
--------------------------------------------------------------------------------
1 | function init() {
2 | setTimeout(() => $('.modal[data-show="true"]').modal('show'), 1000);
3 | };
4 |
5 | window.addEventListener("phx:page-loading-stop", init);
6 | window.addEventListener("load", init);
7 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/sitemap_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SitemapController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | def index(conn, _params) do
6 | render(conn, "index.xml")
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/assets/static/.well-known/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "applinks": {
3 | "apps": [],
4 | "details": [{
5 | "appID": "GXM3CJPMZ7.com.openpace.challenges",
6 | "paths": ["/invite/*"]
7 | }]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/assets/css/core/cards/_card-animations.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Card with hover animations
3 | //
4 |
5 | .card-lift--hover {
6 | &:hover {
7 | transform: translateY(-20px);
8 | @include transition($transition-base);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/assets/js/dashboard.js:
--------------------------------------------------------------------------------
1 | // Import all of the original app.js components
2 | import './app';
3 |
4 | // Dashboard only components
5 | import './components/activity-chart';
6 | import './components/activity-map';
7 | import './components/overview-chart';
8 |
--------------------------------------------------------------------------------
/lib/stripe/card_behavior.ex:
--------------------------------------------------------------------------------
1 | defmodule Stripe.CardBehavior do
2 | @moduledoc """
3 | Behavior to allow us to use mocks for Stripe.Card
4 | """
5 |
6 | @callback create(map()) ::
7 | {:ok, Stripe.Card.t()} | {:error, Stripe.Error.t()}
8 | end
9 |
--------------------------------------------------------------------------------
/assets/css/core/buttons/_button-brand.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Brand buttons
3 | //
4 |
5 |
6 | // Color variations
7 |
8 | @each $color, $value in $brand-colors {
9 | .btn-#{$color} {
10 | @include button-variant($value, $value);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171128013421_add_color_to_paces.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddColorToPaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:paces) do
6 | add :color, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/flash_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.FlashComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | def info_msg(flash), do: live_flash(flash, :info)
6 | def error_msg(flash), do: live_flash(flash, :error)
7 | end
8 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/race/_address.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= @race.address_line1 %>
3 |
4 | <%= if @race.address_line2 do %>
5 | <%= @race.address_line2 %>
6 |
7 | <% end %>
8 |
9 | <%= location(assigns) %>
10 |
11 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/google_auth_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.GoogleAuthView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("auth.json", %{token: token}) do
6 | %{
7 | token: token
8 | }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190402033557_add_content_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddContentToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :content, :text
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/dashboard_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.DashboardController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | def index(conn, _params) do
6 | redirect(conn, to: Routes.overview_path(conn, :index))
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190408212922_add_type_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTypeToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :type, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190709235126_add_logo_url_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddLogoUrlToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :logo_url, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200605032415_add_start_date_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddStartDateToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :start_date, :date
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200603042523_add_course_url_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddCourseUrlToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :course_url, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210118224433_add_amount_to_scores.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddAmountToScores do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:scores) do
6 | add :amount, :float, default: 0
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/assets/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | // Import bootstrap and required libraries
2 | import jquery from 'jquery';
3 | window.jQuery = jquery;
4 | window.$ = jquery;
5 |
6 | import * as Popper from 'popper.js';
7 | window.Popper = Popper;
8 |
9 | import * as bootstrap from 'bootstrap';
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190208052014_add_password_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddPasswordToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :encrypted_password, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190930021245_add_moving_to_trackpoints.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddMovingToTrackpoints do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:trackpoints) do
6 | add :moving, :boolean
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/push_token_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.PushTokenView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("create.json", %{push_token: push_token}) do
6 | %{
7 | token: push_token.token
8 | }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180830035119_add_polyline_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddPolylineToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :polyline, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181218045915_add_timezone_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTimezoneToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :timezone, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190717045528_add_type_to_training_events.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTypeToTrainingEvents do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:training_events) do
6 | add :type, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210118231417_add_polyline_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddPolylineToChallenges do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:challenges) do
6 | add :polyline, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/user_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.UserView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def title("new.html", _) do
6 | "Create an Account"
7 | end
8 |
9 | def title(_page, _assigns) do
10 | gettext("User")
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/strava/auth_behavior.ex:
--------------------------------------------------------------------------------
1 | defmodule Strava.AuthBehavior do
2 | @moduledoc """
3 | Auth behavior to allow us to use mocks for Strava.Auth
4 | """
5 | @callback authorize_url!([]) :: String.t
6 | @callback get_token!([]) :: map()
7 | @callback get_athlete!(map()) :: map()
8 | end
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180813025524_add_experience_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddExperienceToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :experience, :integer
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180819213812_add_sync_at_to_credentials.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddSyncAtToCredentials do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:credentials) do
6 | add :sync_at, :utc_datetime
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190125152555_add_stripe_customer_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddStripeCustomerToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :stripe_customer_id, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190514193155_add_billing_indexes_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddBillingIndexesToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create index(:users, [:customer_id])
6 | create index(:users, [:subscription_id])
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190709235333_add_registration_url_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddRegistrationUrlToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :registration_url, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190815044116_add_description_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDescriptionToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :description, :text
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210118215927_add_segment_id_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddSegmentIdToChallenges do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:challenges) do
6 | add :segment_id, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211208042552_add_description_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDescriptionToChallenges do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:challenges) do
6 | add :description, :text
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/email/welcome.text.eex:
--------------------------------------------------------------------------------
1 | Hi <%= @user.first_name %>,
2 |
3 | Welcome to the <%= company_name() %> family! You're the newest member to our growing community of runners. We are happy to be part of your training plan.
4 |
5 | Happy Running,
6 | The <%= company_name() %> Team
7 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/home/index.html.eex:
--------------------------------------------------------------------------------
1 | <%= render "_schema.html", assigns %>
2 |
3 |
4 | <%= render "_hero.html", assigns %>
5 | <%= render "_dashboard.html", assigns %>
6 | <%= render "_open-source.html", assigns %>
7 | <%= render "_footer-cta.html", assigns %>
8 |
9 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/search_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SearchView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | alias Squeeze.Regions
6 |
7 | def title(_page, _), do: "Search For Your Next Race"
8 |
9 | def states do
10 | Regions.states
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/strava/client_behavior.ex:
--------------------------------------------------------------------------------
1 | defmodule Strava.ClientBehavior do
2 | @moduledoc """
3 | Client behavior to allow us to use mocks for Strava.Client
4 | """
5 | @callback new(String.t, []) :: Tesla.Client
6 | @callback put(Tesla.Env.client(), String.t, any) :: Tesla.Env.result()
7 | end
8 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20191003033815_add_workout_type_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddWorkoutTypeToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :workout_type, :integer
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200606041837_add_geo_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddGeoToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :latitude, :decimal
7 | add :longitude, :decimal
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190827194518_add_elevation_gain_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddElevationGainToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :elevation_gain, :float
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210103165448_add_activity_type_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddActivityTypeToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :activity_type, :integer
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190103225546_add_status_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddStatusToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :status, :integer, null: false, default: 0
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190209193444_add_registered_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddRegisteredToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :registered, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220802163013_add_personal_records_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddPersonalRecordsToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :personal_records, :map
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze/api/auth_error_handler.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Api.AuthErrorHandler do
2 | @moduledoc false
3 |
4 | import Plug.Conn
5 |
6 | def auth_error(conn, {type, _reason}, _opts) do
7 | body = Jason.encode!(%{message: to_string(type)})
8 | send_resp(conn, 401, body)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/menu/_avatar.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= if @current_user.avatar do %>
3 |
4 | <% else %>
5 |
6 | <% end %>
7 |
8 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181224172858_add_imperial_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddImperialToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :imperial, :boolean, default: false, null: true
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190323172949_add_start_at_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddStartAtToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :start_at, :naive_datetime
7 | add :timezone, :string
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20191023024347_add_token_secret_to_credentials.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTokenSecretToCredentials do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:credentials) do
6 | add :token_secret, :string, size: 500
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210304045021_add_start_at_local_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddStartAtLocalToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :start_at_local, :naive_datetime
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/sitemap/index.xml.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= Routes.home_url(@conn, :index) %>
5 | 1.0
6 | daily
7 |
8 |
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190102150636_add_complete_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddCompleteToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :complete, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200528035410_add_qualifier_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddQualifierToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :boston_qualifier, :boolean
7 | add :active, :boolean
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210630031806_add_recurring_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddRecurringToChallenges do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:challenges) do
6 | add :recurring, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211026193306_add_trackpoints_json_to_trackpoint_sets.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTrackpointsJsonToTrackpointSets do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:trackpoint_sets) do
6 | add :trackpoints, :map
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/strava_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.StravaView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("credential.json", %{credential: credential}) do
6 | %{
7 | provider: credential.provider,
8 | uid: credential.uid
9 | }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190223001437_add_default_to_billing_plans.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDefaultToBillingPlans do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:billing_plans) do
6 | add :default, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190831205355_add_gender_bday_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddGenderBdayToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :gender, :integer
7 | add :birthdate, :date
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20230105050138_add_api_enabled_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddApiEnabledToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :api_enabled, :boolean, default: false, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/avatar_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= initials(@user) %>
4 |
5 |
6 | <%= if @user.avatar do %>
7 |
8 | <% end %>
9 |
10 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/strava_webhook_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.StravaWebhookView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("challenge.json", %{challenge: challenge}) do
6 | %{"hub.challenge" => challenge}
7 | end
8 |
9 | def render(_, _) do
10 | %{}
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190416033230_add_trial_end_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTrialEndToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :trial_end, :utc_datetime
7 | add :subscription_status, :integer
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 | Disallow: /dashboard
7 |
8 | Sitemap: https://www.openpace.co/sitemap/index.xml
9 |
--------------------------------------------------------------------------------
/assets/css/core/custom-forms/_custom-select.scss:
--------------------------------------------------------------------------------
1 | .custom-select {
2 | -webkit-appearance: none;
3 | -moz-appearance: none;
4 | appearance: none;
5 | }
6 |
7 | .input-group-append {
8 | .custom-select {
9 | border-top-left-radius: 0;
10 | border-bottom-left-radius: 0;
11 | margin-left: 1px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/settings/user_form_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Settings.UserFormComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | def time_zones do
6 | Tzdata.zone_list()
7 | |> Enum.map(fn(x) ->
8 | {String.replace(x, "_", " "), x}
9 | end)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/dashboard_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.DashboardControllerTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | test "index redirects to overview", %{conn: conn} do
5 | conn = get(conn, dashboard_path(conn, :index))
6 | assert redirected_to(conn) == overview_path(conn, :index)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/assets/css/core/cards/_card-stats.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Card stats
3 | //
4 |
5 | .card-stats {
6 | .card-body {
7 | padding: 1rem 1.5rem;
8 | }
9 |
10 | .card-status-bullet {
11 | position: absolute;
12 | top: 0;
13 | right: 0;
14 | transform: translate(50%, -50%);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/email/reset_password.text.eex:
--------------------------------------------------------------------------------
1 | Hi <%= @user.first_name %>,
2 |
3 | You recently requested to reset your password for your Squeeze account. Use the link below to reset it. This password reset is only valid for the next 24 hours.
4 |
5 | <%= @link %>
6 |
7 | Thanks,
8 |
9 | The <%= company_name() %> Team
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190307045323_create_subscriptions.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateSubscriptions do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:subscriptions) do
6 | add :email, :string
7 | add :type, :string
8 |
9 | timestamps()
10 | end
11 |
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220801163056_add_training_information_to_race_goals.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTrainingInformationToRaceGoals do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:race_goals) do
6 | add :training_paces, :map
7 | add :distance, :float
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/assets/css/core/type/_article.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Article
3 | //
4 |
5 | article {
6 | h4:not(:first-child),
7 | h5:not(:first-child) {
8 | margin-top: 3rem;
9 | }
10 |
11 | h4, h5 {
12 | margin-bottom: 1.5rem;
13 | }
14 |
15 | figure {
16 | margin: 3rem 0;
17 | }
18 |
19 | h5 + figure {
20 | margin-top: 0;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190211040437_add_payment_processor_fields_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddPaymentProcessorFieldsToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :customer_id, :string
7 | add :subscription_id, :string
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190805033850_add_distance_unit_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDistanceUnitToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :planned_distance_unit, :integer
7 | add :distance_unit, :integer
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/squeeze/oauth2/google_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.OAuth2.GoogleTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.OAuth2.Google
5 |
6 | describe "authorize_url/1" do
7 | test "includes accounts.google.com" do
8 | assert Google.authorize_url! =~ ~r/https:\/\/accounts.google.com/
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190323040158_add_breadcrumbs_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddBreadcrumbsToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :distance, :float
7 | add :distance_type, :integer
8 |
9 | add :overview, :text
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190323162043_add_address_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddAddressToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :address_line1, :string
7 | add :address_line2, :string
8 | add :postal_code, :string
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190825032057_add_distance_amount_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDistanceAmountToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :planned_distance_amount, :float
7 | add :distance_amount, :float
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/assets/css/core/mixins/_icon.scss:
--------------------------------------------------------------------------------
1 | @mixin icon-shape-variant($color) {
2 | color: saturate(darken($color, 10%), 10);
3 | background-color: transparentize(lighten($color, 10%), .5);
4 | }
5 |
6 | @mixin icon-font($content, $font-size) {
7 | content: $content;
8 | font-family: $icon-font-family;
9 | font-size: $font-size;
10 | }
11 |
--------------------------------------------------------------------------------
/assets/js/fonts.js:
--------------------------------------------------------------------------------
1 | // Load fonts async
2 | import Iconify from '@iconify/iconify';
3 |
4 | // Iconify must scan the dom each load and replace the icons
5 | // Enable caching in localStorage
6 | Iconify.enableCache('local');
7 |
8 | window.addEventListener("phx:page-loading-stop", Iconify.scan);
9 | window.addEventListener("load", Iconify.scan);
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210304040854_add_slug_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddSlugToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :slug, :string
7 | end
8 |
9 | # Restrict duplicate slugs for a user
10 | create unique_index(:users, :slug)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/assets/css/core/mixins/_badge.scss:
--------------------------------------------------------------------------------
1 | @mixin badge-variant($bg) {
2 | color: saturate(darken($bg, 10%), 10);
3 | background-color: lighten($bg, 32%);
4 |
5 | &[href] {
6 | @include hover-focus {
7 | color: color-yiq($bg);
8 | text-decoration: none;
9 | background-color: darken($bg, 12%);
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171128012003_add_email_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddEmailToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :email, :string
7 | end
8 |
9 | # Restrict duplicate emails for a user
10 | create unique_index(:users, :email)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181118233957_add_refresh_token_to_credentials.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddRefreshTokenToCredentials do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:credentials) do
6 | add :access_token, :string, size: 500
7 | add :refresh_token, :string, size: 500
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/assets/css/core/avatars/_avatar-group.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Avatar group
3 | //
4 |
5 | // General styles
6 |
7 | .avatar-group {
8 | .avatar {
9 | position: relative;
10 | z-index: 2;
11 | border: 2px solid $card-bg;
12 |
13 | &:hover {
14 | z-index: 3;
15 | }
16 | }
17 |
18 | .avatar + .avatar {
19 | margin-left: -1rem;
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_helper.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Helper
3 | // helper classes for different cases
4 | //
5 |
6 |
7 | // Clearfix for sections that use float property
8 |
9 | .floatfix {
10 | &:before,
11 | &:after {
12 | content: '';
13 | display: table;
14 | }
15 | &:after {
16 | clear: both;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/page/support.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render SqueezeWeb.MenuView, "base-navbar.html", assigns %>
4 |
5 |
6 |
Help and Support
7 |
For help and support, please email help@openpace.co.
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/css/core/cards/_card-blockquote.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Card with blockquote
3 | //
4 |
5 | .card-blockquote {
6 | padding: 2rem;
7 | position: relative;
8 |
9 | .svg-bg {
10 | display: block;
11 | width: 100%;
12 | height: 95px;
13 | position: absolute;
14 | top: -94px;
15 | left: 0;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/assets/css/core/navbars/_navbar-floating.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Floating navbar
3 | //
4 |
5 | .navbar-floating-wrapper {
6 | padding-top: 1rem;
7 | padding-bottom: 1rem;
8 | position: absolute;
9 | left: 0;
10 | top: 0;
11 | width: 100%;
12 | z-index: 1;
13 |
14 | .navbar {
15 | border-radius: $border-radius;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180910032145_add_event_id_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddEventIdToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :event_id, references(:events, on_delete: :nothing)
7 | end
8 |
9 | create index(:activities, [:event_id])
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/assets/js/components/timezone-hidden-input.js:
--------------------------------------------------------------------------------
1 | import { u } from 'umbrellajs';
2 | import { guessTimezone } from '../utils';
3 |
4 | function init() {
5 | const timezone = guessTimezone();
6 | u('.timezone-hidden-input').attr('value', timezone);
7 | };
8 |
9 | window.addEventListener("phx:page-loading-stop", init);
10 | window.addEventListener("load", init);
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210729185425_add_follow_count_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddFollowCountToUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :follower_count, :integer, default: 0, null: false
7 | add :following_count, :integer, default: 0, null: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/assets/css/core/popovers/_popover.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Popover
3 | //
4 |
5 |
6 | .popover {
7 | border: 0;
8 | }
9 |
10 | .popover-header {
11 | font-weight: $font-weight-bold;
12 | }
13 |
14 |
15 | // Alternative colors
16 |
17 | @each $color, $value in $theme-colors {
18 | .popover-#{$color} {
19 | @include popover-variant($value);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/assets/js/components/base.js:
--------------------------------------------------------------------------------
1 | import './alert';
2 | import './autocomplete-input';
3 | import './avatar';
4 | import './btn-spinner';
5 | import './copy-input';
6 | import './date-picker';
7 | import './distance-select';
8 | import './imperial-hidden-input';
9 | import './modal';
10 | import './timezone-hidden-input';
11 | import './time-input';
12 | import './typewriter';
13 |
--------------------------------------------------------------------------------
/lib/strava/streams_behavior.ex:
--------------------------------------------------------------------------------
1 | defmodule Strava.StreamsBehavior do
2 | @moduledoc """
3 | Streams behavior to allow us to use mocks for Strava.Streams
4 | """
5 |
6 | @callback get_activity_streams(
7 | Tesla.Env.client(),
8 | integer(),
9 | list(String.t()),
10 | boolean()
11 | ) :: {:ok, Strava.StreamSet.t()} | {:error, Tesla.Env.t()}
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190223223014_create_webhook_events.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateWebhookEvents do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:webhook_events) do
6 | add :provider, :string
7 | add :provider_id, :string
8 | add :body, :text
9 |
10 | timestamps()
11 | end
12 |
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200916033210_change_score_to_float.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeScoreToFloat do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:scores) do
6 | modify :score, :float
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:scores) do
12 | modify :score, :integer
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220727223626_add_slug_to_race_goals.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddSlugToRaceGoals do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:race_goals) do
6 | add :slug, :string
7 | end
8 |
9 | # Restrict duplicate slugs for a race_goal
10 | create unique_index(:race_goals, :slug)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221224224446_add_slug_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddSlugToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :slug, :string
7 | end
8 |
9 | # Restrict duplicate slugs for a activities
10 | create unique_index(:activities, :slug)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/squeeze/races/trackpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Races.Trackpoint do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 |
6 | # @required_fields ~w()a
7 | # @optional_fields ~w(altitude coordinates distance)a
8 |
9 | schema "race_trackpoints" do
10 | field :altitude, :float
11 | field :coordinates, :map
12 | field :distance, :float
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181223185441_add_planning_fields_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddPlanningFieldsToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :planned_distance, :float
7 | add :planned_duration, :integer
8 | add :planned_date, :date
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190203082729_change_uid_to_string.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeUidToString do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:credentials) do
6 | modify :uid, :string
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:credentials) do
12 | modify :uid, :integer
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/assets/css/core/badges/_badge-floating.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Badge floating
3 | //
4 |
5 |
6 | .btn {
7 | .badge-floating {
8 | position: absolute;
9 | top: -50%;
10 | transform: translateY(50%);
11 | border: 3px solid;
12 |
13 | &.badge:not(.badge-circle) {
14 | transform: translate(147%, 50%);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_transform.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Transform
3 | //
4 |
5 |
6 | @include media-breakpoint-up(lg) {
7 | .transform-perspective-right {
8 | transform: scale(1) perspective(1040px) rotateY(-11deg) rotateX(2deg) rotate(2deg);
9 | }
10 | .transform-perspective-left{
11 | transform: scale(1) perspective(2000px) rotateY(11deg) rotateX(2deg) rotate(-2deg)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/cache_body_reader.ex:
--------------------------------------------------------------------------------
1 | # https://hexdocs.pm/plug/Plug.Parsers.html#module-custom-body-reader
2 | defmodule CacheBodyReader do
3 | @moduledoc false
4 |
5 | alias Plug.Conn
6 |
7 | def read_body(conn, opts) do
8 | {:ok, body, conn} = Conn.read_body(conn, opts)
9 | conn = update_in(conn.assigns[:raw_body], &[body | (&1 || [])])
10 | {:ok, body, conn}
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181201175430_change_polyline_to_text.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangePolylineToText do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:activities) do
6 | modify :polyline, :text
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:activities) do
12 | modify :polyline, :string
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190628000419_add_external_id_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddExternalIdToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :external_id, :string
7 | end
8 |
9 | # Restrict duplicate races with the same external id
10 | create unique_index(:races, [:external_id])
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200529044028_add_course_profile_to_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddCourseProfileToRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:races) do
6 | add :certified, :boolean
7 | add :course_profile, :integer
8 | add :course_terrain, :integer
9 | add :course_type, :integer
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/assets/js/hooks/base.js:
--------------------------------------------------------------------------------
1 | import CalendarChart from './calendar-chart';
2 | import DurationSelect from './duration-select';
3 | import Modal from './modal';
4 | import RecentActivityChart from './recent-activity-chart';
5 | import SlimSelect from './slim-select';
6 |
7 | export default {
8 | CalendarChart,
9 | DurationSelect,
10 | Modal,
11 | RecentActivityChart,
12 | SlimSelect,
13 | };
14 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/home/form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, [class: "ui form"], fn f -> %>
2 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180817041247_change_distance_to_integer.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeDistanceToInteger do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:user_prefs) do
6 | modify :distance, :integer
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:user_prefs) do
12 | modify :distance, :float
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/squeeze/races/races_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.RacesTest do
2 | use Squeeze.DataCase
3 |
4 | import Squeeze.Factory
5 |
6 | alias Squeeze.Races
7 |
8 | describe "races" do
9 | test "get_race!/1 returns the race with given slug" do
10 | race = insert(:race)
11 | slug = race.slug
12 | assert Races.get_race!(slug).slug == slug
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/home/_dashboard.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![<%= gettext(]()
" src="<%= Routes.static_path(@conn, "/images/home/overview@1x.jpg") %>" class="img-fluid floating shadow">
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/shared/avatar.html.eex:
--------------------------------------------------------------------------------
1 | <% user = assigns[:user] %>
2 |
3 | <%= if user.avatar do %>
4 |
5 |

6 |
7 | <% else %>
8 |
9 | <%= initials(user) %>
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210212233646_change_challenge_polyline_type.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeChallengePolylineType do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:challenges) do
6 | modify :polyline, :text
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:challenges) do
12 | modify :polyline, :string
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190223235513_change_external_id_to_string.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeExternalIdToString do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:activities) do
6 | modify :external_id, :string
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:activities) do
12 | modify :external_id, :integer
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/squeeze/api/auth_pipeline.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Api.AuthPipeline do
2 | @moduledoc false
3 |
4 | use Guardian.Plug.Pipeline, otp_app: :squeeze,
5 | module: Squeeze.Guardian,
6 | error_handler: Squeeze.Api.AuthErrorHandler
7 |
8 | plug Guardian.Plug.VerifySession
9 | plug Guardian.Plug.VerifyHeader
10 | plug Guardian.Plug.EnsureAuthenticated
11 | plug Guardian.Plug.LoadResource
12 | end
13 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/weekly_summary_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 | Summary
4 |
5 |
6 | <%= unless Enum.empty?(@activities) do %>
7 |
8 |
9 | <%= completed_distance(assigns) %><%= distance_label(assigns) %>
10 |
11 |
12 | <% end %>
13 |
14 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/user_prefs_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.UserPrefsView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("user_prefs.json", %{user_prefs: user_prefs}) do
6 | %{
7 | timezone: user_prefs.timezone,
8 | imperial: user_prefs.imperial,
9 | gender: user_prefs.gender,
10 | birthdate: user_prefs.birthdate
11 | }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190122140903_add_basic_fields_to_training_plans.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddBasicFieldsToTrainingPlans do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:training_plans) do
6 | add :experience_level, :integer, default: 0, null: false
7 | add :week_count, :integer, null: false
8 | add :description, :text
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190501142943_add_trackpoints_belong_to_set.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddTrackpointsBelongToSet do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:trackpoints) do
6 | add :trackpoint_set_id, references(:trackpoint_sets, on_delete: :delete_all), null: false
7 | end
8 |
9 | create index(:trackpoints, [:trackpoint_set_id])
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/factories/follow_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.FollowFactory do
2 | @moduledoc false
3 |
4 | alias Squeeze.Social.Follow
5 |
6 | defmacro __using__(_opts) do
7 | quote do
8 | def follow_factory do
9 | %Follow{
10 | follower: build(:user),
11 | followee: build(:user),
12 | pending: false
13 | }
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/squeeze/company_helper_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.CompanyHelperTest do
2 | use Squeeze.DataCase, async: true
3 |
4 | alias Squeeze.CompanyHelper
5 |
6 | test "#company_name/0" do
7 | assert CompanyHelper.company_name() == "OpenPace"
8 | end
9 |
10 | test "#team_email/0" do
11 | assert CompanyHelper.team_email() ==
12 | {"The OpenPace Team", "team@openpace.co"}
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/challenge_share_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ChallengeShareController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.Challenges
6 |
7 | def show(conn, %{"slug" => slug}) do
8 | challenge = Challenges.get_challenge_by_slug!(slug)
9 | render(conn, "show.html", challenge: challenge, page_title: "Challenge Invite")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/assets/css/custom/_mixins.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Custom mixins
3 | //
4 |
5 |
6 | @import "../core/mixins/alert";
7 | @import "../core/mixins/badge";
8 | @import "../core/mixins/background-variant";
9 | @import "../core/mixins/buttons";
10 | @import "../core/mixins/custom-forms";
11 | @import "../core/mixins/forms";
12 | @import "../core/mixins/icon";
13 | @import "../core/mixins/modals";
14 | @import "../core/mixins/popover";
15 |
--------------------------------------------------------------------------------
/assets/js/components/imperial-hidden-input.js:
--------------------------------------------------------------------------------
1 | import { u } from 'umbrellajs';
2 | import { guessTimezone } from '../utils';
3 |
4 | function init() {
5 | const useImperialMeasurements = guessTimezone().indexOf('America') !== -1;
6 | u('.imperial-hidden-input').attr('value', useImperialMeasurements);
7 | };
8 |
9 | window.addEventListener("phx:page-loading-stop", init);
10 | window.addEventListener("load", init);
11 |
--------------------------------------------------------------------------------
/lib/mix/tasks/add_slugs_to_activities.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.AddSlugsToActivities do
2 | use Mix.Task
3 | @moduledoc false
4 |
5 | alias Squeeze.Dashboard.Activity
6 | alias Squeeze.Repo
7 |
8 | @doc false
9 | def run(_) do
10 | Mix.Task.run("app.start")
11 |
12 | Repo.all(Activity)
13 | |> Enum.map(&Activity.changeset/1)
14 | |> Enum.map(&Repo.add_slug_to_existing/1)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/squeeze/company_helper.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.CompanyHelper do
2 | @moduledoc """
3 | Module to handle company name and other similar strings.
4 | """
5 |
6 | def company_name, do: "OpenPace"
7 | def team_email, do: {"The OpenPace Team", "team@openpace.co"}
8 | def copyright_year, do: Date.utc_today.year
9 | def website_name, do: "OpenPace"
10 | def website_url, do: "https://www.openpace.co"
11 | end
12 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/region_search_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.RegionSearchView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def title(_page, assigns) do
6 | "Best races in #{region_name(assigns)}"
7 | end
8 |
9 | def region_name(%{region: region}) do
10 | region.long_name
11 | end
12 |
13 | def h1(assigns) do
14 | "Best races in #{region_name(assigns)}"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171109012545_add_external_id_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddExternalIdToActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:activities) do
6 | add :external_id, :integer, null: false
7 | end
8 |
9 | # Restrict duplicate activities per user
10 | create unique_index(:activities, [:user_id, :external_id])
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190501142700_create_trackpoint_sets.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateTrackpointSets do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:trackpoint_sets) do
6 | add :activity_id, references(:activities, on_delete: :delete_all), null: false
7 |
8 | timestamps()
9 | end
10 |
11 | create index(:trackpoint_sets, [:activity_id])
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/factories/score_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.ScoreFactory do
2 | @moduledoc false
3 |
4 | alias Squeeze.Challenges.Score
5 |
6 | defmacro __using__(_opts) do
7 | quote do
8 | def score_factory do
9 | %Score{
10 | score: :rand.uniform(500),
11 | challenge: build(:challenge),
12 | user: build(:user)
13 | }
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/sitemap_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SitemapControllerTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | describe "#index.xml" do
5 | test "renders url for the homepage", %{conn: conn} do
6 | conn = get(conn, sitemap_path(conn, :index))
7 |
8 | assert conn.resp_body =~ "http://www.example.com/"
9 | assert conn.status == 200
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/factories/push_token_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.PushTokenFactory do
2 | @moduledoc false
3 |
4 | alias Squeeze.Notifications.PushToken
5 |
6 | defmacro __using__(_opts) do
7 | quote do
8 | def push_token_factory do
9 | %PushToken{
10 | token: sequence(:push_token, &"ExpoToken[#{&1}]"),
11 | user: build(:user)
12 | }
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/svg_polyline_component.html.heex:
--------------------------------------------------------------------------------
1 | <% data = svg_path(@polyline) %>
2 | <%= if data do %>
3 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171105203801_create_paces.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreatePaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:paces) do
6 | add :name, :string
7 | add :offset, :integer
8 | add :user_id, references(:users, on_delete: :delete_all), null: false
9 |
10 | timestamps()
11 | end
12 |
13 | create index(:paces, [:user_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190121231335_create_training_plans.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateTrainingPlans do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:training_plans) do
6 | add :name, :string
7 | add :user_id, references(:users, on_delete: :nothing), null: false
8 |
9 | timestamps()
10 | end
11 |
12 | create index(:training_plans, [:user_id])
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/squeeze/duration_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.DurationTest do
2 | use ExUnit.Case
3 |
4 | alias Squeeze.Duration
5 |
6 | test "cast/1" do
7 | assert Duration.cast(1) == {:ok, 1}
8 | assert Duration.cast("01:00") == {:ok, 60}
9 | assert Duration.cast("01:11") == {:ok, 71}
10 | assert Duration.cast("3:01:11") == {:ok, 10_871}
11 | assert Duration.cast("35:00:00") == {:ok, 126_000}
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | env:
9 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
10 |
11 | jobs:
12 | deploy:
13 | name: Deploy app
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: superfly/flyctl-actions/setup-flyctl@master
18 | - run: flyctl deploy --remote-only
19 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/search_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.SearchController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.RaceSearch
6 |
7 | def index(conn, _) do
8 | case RaceSearch.search() do
9 | {:ok, results} ->
10 | render(conn, "index.html", results: results)
11 | _ ->
12 | render(conn, "index.html", results: [])
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/assets/css/core/vendors/_sweet-alert-2.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Sweet alert 2
3 | // Sweet alert 2 plugin overrides
4 | //
5 |
6 |
7 | .swal2-popup {
8 |
9 | padding: $swal2-padding;
10 |
11 | .swal2-title {
12 | font-size: $swal2-title-font-size;
13 | }
14 |
15 | .swal2-content {
16 | font-size: $swal2-content-font-size;
17 | }
18 |
19 | .swal2-image {
20 | max-width: 200px;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/assets/static/.well-known/assetlinks.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "relation": [
4 | "delegate_permission/common.handle_all_urls"
5 | ],
6 | "target": {
7 | "namespace": "android_app",
8 | "package_name": "com.openpace.challenges",
9 | "sha256_cert_fingerprints": [
10 | "28:55:2A:6E:7C:C0:6A:99:E5:F8:A0:CA:29:6D:75:DD:99:9C:1C:A1:E7:9F:72:32:FD:F8:48:C9:11:3B:AC:BF"
11 | ]
12 | }
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/lib/squeeze/dashboard/trackpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Dashboard.Trackpoint do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 |
6 | embedded_schema do
7 | field :altitude, :float
8 | field :cadence, :integer
9 | field :coordinates, :map
10 | field :distance, :float
11 | field :heartrate, :integer
12 | field :moving, :boolean
13 | field :time, :integer
14 | field :velocity, :float
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/activities/chart_component.html.heex:
--------------------------------------------------------------------------------
1 |
11 |
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220301173633_add_namer_fields_to_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddNamerFieldsToUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:user_prefs) do
6 | add :rename_activities, :boolean, default: false, null: false
7 | add :emoji, :boolean, default: true, null: false
8 | add :branding, :boolean, default: true, null: false
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/challenges/static_map_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
})
4 |
5 |
6 |
7 |
8 | <%= @segment.name %>
9 |
10 |
11 | <%= description(@user, @segment) %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/home/_schema.html.eex:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200916034108_add_share_fields_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddShareFieldsToChallenges do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:challenges) do
6 | add :slug, :string
7 | add :private, :boolean, default: false, null: false
8 | end
9 |
10 | # Restrict duplicate slugs for a challenge
11 | create unique_index(:challenges, :slug)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/assets/css/core/vendors/_chartjs.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Chart.js
3 | //
4 |
5 |
6 | #chartjs-tooltip {
7 | opacity: 1;
8 | position: absolute;
9 | background: rgba(0, 0, 0, .7);
10 | color: white;
11 | border-radius: 3px;
12 | transition: all .1s ease;
13 | pointer-events: none;
14 | transform: translate(-50%, 0);
15 | }
16 |
17 | .chartjs-tooltip-key {
18 | display: inline-block;
19 | width: 10px;
20 | height: 10px;
21 | margin-right: 10px;
22 | }
23 |
--------------------------------------------------------------------------------
/lib/squeeze/slug_generator.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.SlugGenerator do
2 | @moduledoc """
3 | Generates unique slugs for object urls
4 | """
5 |
6 | @doc """
7 | Generates a six character long alphanumeric string
8 |
9 | ## Examples
10 |
11 | iex> gen_slug()
12 | "hrz9f6"
13 |
14 | """
15 | def gen_slug(length \\ 6) do
16 | for _ <- 1..length, into: "", do: <>
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/activity_chart/chart.html.eex:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181223192749_change_external_id_for_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeExternalIdForActivities do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:activities) do
6 | modify :external_id, :integer, default: nil, null: true
7 | end
8 | end
9 |
10 | def down do
11 | alter table(:activities) do
12 | modify :external_id, :integer, null: false
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190218002113_create_billing_plans.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateBillingPlans do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:billing_plans) do
6 | add :name, :string
7 | add :amount, :integer
8 | add :provider_id, :string
9 | add :interval, :string
10 |
11 | timestamps()
12 | end
13 |
14 | create index(:billing_plans, [:provider_id])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210217044932_add_dates_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDatesToChallenges do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:challenges) do
6 | add :start_date, :date
7 | add :end_date, :date
8 | end
9 | end
10 |
11 | def down do
12 | alter table(:challenges) do
13 | remove :start_date, :date
14 | remove :end_date, :date
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220924032609_add_activity_index_to_trackpoint_sets.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddActivityIndexToTrackpointSets do
2 | use Ecto.Migration
3 |
4 | def up do
5 | drop index(:trackpoint_sets, [:activity_id])
6 | create unique_index(:trackpoint_sets, [:activity_id])
7 | end
8 |
9 | def down do
10 | drop unique_index(:trackpoint_sets, [:activity_id])
11 | create index(:trackpoint_sets, [:activity_id])
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/factories/training_plan_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.TrainingPlanFactory do
2 | @moduledoc false
3 |
4 | alias Squeeze.TrainingPlans.Plan
5 |
6 | defmacro __using__(_opts) do
7 | quote do
8 | def training_plan_factory do
9 | miles = Enum.random(2..16)
10 |
11 | %Plan{
12 | name: "#{miles} mi run",
13 | week_count: 18,
14 | user: build(:user)
15 | }
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/api/challenge_activity_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.ChallengeActivityController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.Challenges
6 |
7 | def index(conn, %{"id" => slug}) do
8 | challenge = Challenges.get_challenge_by_slug!(slug)
9 | activities = Challenges.list_challenge_activities(challenge)
10 | render(conn, "index.json", %{challenge_activities: activities})
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171022220118_create_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateUsers do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users) do
6 | add :first_name, :string
7 | add :last_name, :string
8 | add :description, :string
9 | add :avatar, :string
10 | add :city, :string
11 | add :state, :string
12 | add :country, :string
13 |
14 | timestamps()
15 | end
16 |
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/strava/activities_behavior.ex:
--------------------------------------------------------------------------------
1 | defmodule Strava.ActivitiesBehavior do
2 | @moduledoc """
3 | Activities behavior to allow us to use mocks for Strava.Activities
4 | """
5 |
6 | @callback get_logged_in_athlete_activities(Tesla.Env.client(), keyword()) ::
7 | {:ok, list(Strava.SummaryActivity.t())} | {:error, Tesla.Env.t()}
8 |
9 | @callback get_activity_by_id(Tesla.Env.client(), integer()) ::
10 | {:ok, Strava.DetailedActivity.t()} | {:error, Tesla.Env.t()}
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190324012558_create_race_trackpoints.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateRaceTrackpoints do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:race_trackpoints) do
6 | add :altitude, :float
7 | add :coordinates, :map
8 | add :distance, :float
9 |
10 | add :race_id, references(:races, on_delete: :delete_all), null: false
11 | end
12 |
13 | create index(:race_trackpoints, [:race_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210123063435_create_push_tokens.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreatePushTokens do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:push_tokens) do
6 | add :token, :string
7 | add :user_id, references(:users, on_delete: :delete_all), primary_key: true
8 |
9 | timestamps()
10 | end
11 |
12 | create index(:push_tokens, [:user_id])
13 | create unique_index(:push_tokens, [:user_id, :token])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | // We need to import the CSS so that webpack will load it.
2 | // The MiniCssExtractPlugin is used to separate it out into
3 | // its own CSS file.
4 | import '../css/app.scss';
5 |
6 | // Import dependencies
7 | import 'phoenix_html';
8 |
9 | // Import bootstrap and required libraries
10 | import './bootstrap';
11 |
12 | // Import base components
13 | import './components/base';
14 |
15 | // Fonts
16 | import './fonts';
17 |
18 | // Live View
19 | import './live_view';
20 |
--------------------------------------------------------------------------------
/lib/squeeze/auth_pipeline.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.AuthPipeline do
2 | @moduledoc """
3 | This module defines the pipeline for auth allowing a shared error handler
4 | across all plugs.
5 | """
6 |
7 | use Guardian.Plug.Pipeline, otp_app: :squeeze,
8 | module: Squeeze.Guardian,
9 | error_handler: Squeeze.AuthErrorHandler
10 |
11 | plug Guardian.Plug.VerifySession
12 | plug SqueezeWeb.Plug.VerifyRememberMe
13 | plug Guardian.Plug.LoadResource, allow_blank: true
14 | end
15 |
--------------------------------------------------------------------------------
/assets/css/core/custom-forms/_custom-checkbox.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Custom checkbox
3 | //
4 |
5 | .custom-checkbox {
6 | .custom-control-input ~ .custom-control-label {
7 | cursor: pointer;
8 | font-size: $font-size-sm;
9 | height: $custom-control-indicator-size;
10 | }
11 | }
12 |
13 |
14 | // Color variations
15 |
16 | @each $color, $value in $theme-colors {
17 | .custom-checkbox-#{$color} {
18 | @include custom-checkbox-variant($value);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/squeeze/live_auth_pipeline.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.LiveAuthPipeline do
2 | @moduledoc """
3 | This module defines the pipeline for live view auth allowing a shared error handler
4 | across all plugs.
5 | """
6 |
7 | use Guardian.Plug.Pipeline, otp_app: :squeeze,
8 | module: Squeeze.Guardian,
9 | error_handler: Squeeze.AuthErrorHandler
10 |
11 | plug Guardian.Plug.VerifySession
12 | plug SqueezeWeb.Plug.VerifyRememberMe
13 | plug Guardian.Plug.EnsureAuthenticated
14 | end
15 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ErrorView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("404.html", _assigns) do
6 | "Page not found"
7 | end
8 |
9 | def render("500.html", _assigns) do
10 | "Internal server error"
11 | end
12 |
13 | # In case no render clause matches or no
14 | # template is found, let's render it as 500
15 | def template_not_found(_template, assigns) do
16 | render "500.html", assigns
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/garmin_webhook_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.GarminWebhookController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.Logger
6 |
7 | plug :log_webhook_event
8 |
9 | def webhook(conn, _params) do
10 | render(conn, "success.json")
11 | end
12 |
13 | defp log_webhook_event(conn, _) do
14 | body = Jason.encode!(conn.params)
15 | Logger.log_webhook_event(%{provider: "garmin", body: body})
16 | conn
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180819194130_remove_paces_from_events.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.RemovePacesFromEvents do
2 | use Ecto.Migration
3 |
4 | def up do
5 | drop index(:events, [:pace_id])
6 | alter table(:events) do
7 | remove :pace_id
8 | end
9 | end
10 |
11 | def down do
12 | alter table(:events) do
13 | add :pace_id, references(:paces, on_delete: :delete_all), null: false
14 | end
15 |
16 | create index(:events, [:pace_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/assets/css/core/tables/_table-actions.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Table actions
3 | //
4 |
5 | .table-action {
6 | font-size: $font-size-sm;
7 | color: $table-action-color;
8 | margin: 0 .25rem;
9 |
10 | &:hover {
11 | color: darken($table-action-color, 10%);
12 | }
13 | }
14 |
15 | .table-action-delete {
16 | &:hover {
17 | color: theme-color("danger");
18 | }
19 | }
20 |
21 | .table-dark {
22 | .table-action {
23 | color: $table-dark-action-color;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171105204828_create_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:activities) do
6 | add :name, :string
7 | add :distance, :float
8 | add :duration, :integer
9 | add :start_at, :naive_datetime
10 | add :user_id, references(:users, on_delete: :nothing)
11 |
12 | timestamps()
13 | end
14 |
15 | create index(:activities, [:user_id])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/squeeze/utils_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.UtilsTest do
2 | use Squeeze.DataCase
3 |
4 | @moduledoc false
5 |
6 | import Squeeze.Utils, only: [cast_float: 1]
7 |
8 | describe "#cast_float/1" do
9 | test "returns nil for nil" do
10 | assert cast_float(nil) == nil
11 | end
12 |
13 | test "casts integers to floats" do
14 | assert cast_float(1) == 1.0
15 | end
16 |
17 | test "works with floats" do
18 | assert cast_float(1.0) == 1.0
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/assets/css/core/type/_type.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Type
3 | //
4 |
5 |
6 | // Paragraphs
7 |
8 | p {
9 | font-size: $paragraph-font-size;
10 | font-weight: $paragraph-font-weight;
11 | line-height: $paragraph-line-height;
12 | }
13 |
14 | .lead {
15 | font-size: $lead-font-size;
16 | font-weight: $lead-font-weight;
17 | line-height: $paragraph-line-height;
18 | margin-top: 1.5rem;
19 |
20 | + .btn-wrapper {
21 | margin-top: 3rem;
22 | }
23 | }
24 |
25 | .description {
26 | font-size: $font-size-sm;
27 | }
28 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/api/user_prefs_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.UserPrefsController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.Accounts
6 |
7 | action_fallback SqueezeWeb.Api.FallbackController
8 |
9 | def update(conn, %{"user_prefs" => params}) do
10 | user = conn.assigns.current_user
11 |
12 | with {:ok, _} <- Accounts.update_user_prefs(user.user_prefs, params) do
13 | send_resp(conn, :no_content, "")
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.PageController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | def privacy_policy(conn, _params) do
6 | render(conn, "privacy_policy.html", page_title: "Privacy Policy")
7 | end
8 |
9 | def support(conn, _params) do
10 | render(conn, "support.html", page_title: "Support")
11 | end
12 |
13 | def terms(conn, _params) do
14 | render(conn, "terms.html", page_title: "Terms and Conditions")
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/challenges/show_live.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Challenges.ShowLive do
2 | use SqueezeWeb, :live_view
3 | @moduledoc false
4 |
5 | alias Squeeze.Challenges
6 |
7 | @impl true
8 | def mount(%{"id" => slug}, _session, socket) do
9 | user = socket.assigns.current_user
10 | challenge = Challenges.get_challenge_by_slug!(slug)
11 |
12 | socket = socket
13 | |> assign(:current_user, user)
14 | |> assign(challenge: challenge)
15 |
16 | {:ok, socket}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/race/_faq.html.eex:
--------------------------------------------------------------------------------
1 | <% start_at = start_at(assigns) %>
2 | <%= if start_at do %>
3 | When is the <%= @race.name %>?
4 | The <%= @race.name %> starts on <%= date(assigns) %> at <%= time(assigns) %>.
5 | <% end %>
6 |
7 | Is the <%= @race.name %> a Boston Qualifier?
8 | <%= if @race.boston_qualifier do %>
9 | Yes, the <%= @race.name %> is a Boston Qualifier.
10 | <% else %>
11 | No, the <%= @race.name %> is not a Boston Qualifier.
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_position.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Position
3 | // modifier classes to be applied on an abosolute positioned element
4 | // use it next to .position-absolute class
5 | //
6 |
7 | @each $size, $value in $spacers {
8 | .top-#{$size} {
9 | top: $value;
10 | }
11 | .right-#{$size} {
12 | right: $value;
13 | }
14 | .bottom-#{$size} {
15 | bottom: $value;
16 | }
17 | .left-#{$size} {
18 | left: $value;
19 | }
20 | }
21 |
22 | .center {
23 | left: 50%;
24 | transform: translateX(-50%);
25 | }
26 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/modal_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ModalView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def activity_types do
6 | [
7 | "Run",
8 | "Bike",
9 | "Swim",
10 | "Cross Training",
11 | "Walk",
12 | "Strength Training",
13 | "Workout",
14 | "Yoga"
15 | ]
16 | end
17 |
18 | def workout_types do
19 | [
20 | "Race": "race",
21 | "Long Run": "long_run",
22 | "Workout": "workout"
23 | ]
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/squeeze_web/plugs/auth_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Plug.AuthTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | alias SqueezeWeb.Plug.Auth
5 |
6 | test "gets authenticated user from session", %{conn: conn} do
7 | conn = conn
8 | |> call_auth_plug()
9 |
10 | assert assigned_current_user?(conn)
11 | end
12 |
13 | defp assigned_current_user?(conn) do
14 | assert conn.assigns[:current_user] != nil
15 | end
16 |
17 | defp call_auth_plug(conn) do
18 | Auth.call(conn, %{})
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/settings/namer_card_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Settings.NamerCardComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | alias Squeeze.Accounts.UserPrefs
6 |
7 | def gender_opts do
8 | Ecto.Enum.mappings(UserPrefs, :gender)
9 | |> Enum.map(fn ({k, _}) -> {format_option(k), k} end)
10 | end
11 |
12 | defp format_option(opt) do
13 | opt
14 | |> Atom.to_string()
15 | |> String.replace("_", " ")
16 | |> String.capitalize()
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/factories/billing_plan_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.BillingPlanFactory do
2 | @moduledoc false
3 |
4 | alias Faker.Lorem
5 | alias Squeeze.Billing.Plan
6 |
7 | defmacro __using__(_opts) do
8 | quote do
9 | def billing_plan_factory do
10 | %Plan{
11 | name: "Base Monthly Fee",
12 | amount: 1_000,
13 | provider_id: "plan_#{Lorem.characters(15)}",
14 | interval: "month",
15 | default: false
16 | }
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/squeeze/email_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.EmailTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.Email
5 |
6 | import Squeeze.Factory
7 |
8 | test "welcome email" do
9 | user = insert(:user)
10 |
11 | email = Email.welcome_email(user)
12 |
13 | assert email.to == user.email
14 | assert email.from == {"The OpenPace Team", "team@openpace.co"}
15 | assert email.html_body =~ "Welcome to the OpenPace family!"
16 | assert email.text_body =~ "Welcome to the OpenPace family!"
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/assets/css/core/mixins/_modals.scss:
--------------------------------------------------------------------------------
1 | @mixin modal-variant($background) {
2 | .modal-title {
3 | color: color-yiq($background);
4 | }
5 |
6 | .modal-header,
7 | .modal-footer {
8 | border-color: rgba(color-yiq($background), .075);
9 | }
10 |
11 | .modal-content {
12 | background-color: $background;
13 | color: color-yiq($background);
14 |
15 | .heading {
16 | color: color-yiq($background);
17 | }
18 | }
19 |
20 | .close {
21 | & > span:not(.sr-only) {
22 | color: $white;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/squeeze/mailing_list/subscription.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.MailingList.Subscription do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 |
7 | schema "subscriptions" do
8 | field :email, :string
9 | field :type, :string
10 |
11 | timestamps()
12 | end
13 |
14 | @doc false
15 | def changeset(subscription, attrs) do
16 | subscription
17 | |> cast(attrs, [:email, :type])
18 | |> validate_format(:email, ~r/@/)
19 | |> validate_required([:email, :type])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/dashboard/load_history_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Dashboard.LoadHistoryComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | def show_component?(%{current_user: user}) do
6 | credential = Enum.find(user.credentials, &(&1.provider == "strava"))
7 | credential && is_nil(credential.sync_at)
8 | end
9 |
10 | @impl true
11 | def handle_event("load_history", _params, socket) do
12 | send(self(), :start_history_loader)
13 | {:noreply, socket}
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/follow_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.FollowView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | alias SqueezeWeb.Api.UserView
6 |
7 | def render("followers.json", %{users: users}) do
8 | %{followers: render_many(users, UserView, "user.json", as: :user)}
9 | end
10 |
11 | def render("following.json", %{users: users}) do
12 | %{following: render_many(users, UserView, "user.json", as: :user)}
13 | end
14 |
15 | def render("follow.json", _) do
16 | %{}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/assets/css/core/content/_main-content.scss:
--------------------------------------------------------------------------------
1 | .main-content {
2 | position: relative;
3 |
4 | // Navbar
5 | .navbar-top {
6 | padding-left: 0 !important;
7 | padding-right: 0 !important;
8 | }
9 |
10 | // Container
11 | .container-fluid {
12 | @include media-breakpoint-up(md) {
13 | padding-left: ($main-content-padding-x + $grid-gutter-width / 2) !important;
14 | padding-right: ($main-content-padding-x + $grid-gutter-width / 2) !important;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/assets/js/components/alert.js:
--------------------------------------------------------------------------------
1 | import { u } from 'umbrellajs';
2 |
3 | function fadeOut(alert) {
4 | alert.removeClass('show');
5 | }
6 |
7 | function removeElement(alert) {
8 | alert.remove();
9 | }
10 |
11 | function init() {
12 | const timeout = setTimeout(() => {
13 | const alert = u('.alert[data-auto-hide="true"]');
14 | fadeOut(alert);
15 | setTimeout(() => removeElement(alert), 150);
16 | }, 4000);
17 | };
18 |
19 | window.addEventListener("phx:page-loading-stop", init);
20 | window.addEventListener("load", init);
21 |
--------------------------------------------------------------------------------
/lib/squeeze/garmin/client.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Garmin.Client do
2 | @moduledoc false
3 |
4 | use Tesla
5 |
6 | alias Squeeze.Accounts.Credential
7 | alias Squeeze.Garmin.Middleware
8 |
9 | plug Tesla.Middleware.JSON
10 |
11 | def new(%Credential{token: token, token_secret: token_secret}) do
12 | new(token: token, token_secret: token_secret)
13 | end
14 |
15 | def new(opts) when is_list(opts) do
16 | Tesla.client([
17 | {Middleware.OAuth, opts}
18 | ])
19 | end
20 |
21 | def new, do: new([])
22 | end
23 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/map/activity-map.html.eex:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | <% pace = pace(assigns) %>
9 | <%= for color <- gradient() do %>
10 |
11 | <%= format_duration(pace / color.factor) %>
12 |
13 | <% end %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180806045623_create_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateUserPrefs do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:user_prefs) do
6 | add :distance, :float
7 | add :duration, :integer
8 | add :personal_record, :integer
9 | add :name, :string
10 | add :race_date, :date
11 | add :user_id, references(:users, on_delete: :nothing)
12 |
13 | timestamps()
14 | end
15 |
16 | create index(:user_prefs, [:user_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/squeeze/billing/invoice_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Accounts.InvoiceTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.Billing.Invoice
5 |
6 | import Squeeze.Factory
7 |
8 | @valid_attrs params_for(:invoice)
9 |
10 | test "changeset with full attributes" do
11 | changeset = Invoice.changeset(%Invoice{}, @valid_attrs)
12 | assert changeset.valid?
13 | end
14 |
15 | test "changeset with no attributes" do
16 | changeset = Invoice.changeset(%Invoice{}, %{})
17 | refute changeset.valid?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/auth_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.AuthControllerTest do
2 | use SqueezeWeb.ConnCase
3 | import Mox
4 |
5 | # This makes us check whether our mocks have been properly called at the end
6 | # of each test.
7 | setup :verify_on_exit!
8 |
9 | describe "GET #request" do
10 | test "with provider google", %{conn: conn} do
11 | conn = get(conn, auth_path(conn, :request, "google"))
12 | assert redirected_to(conn) =~ ~r/https:\/\/accounts.google.com/
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/mix/tasks/update_start_dates.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.UpdateStartDates do
2 | use Mix.Task
3 | @moduledoc false
4 |
5 | alias Squeeze.Challenges.Challenge
6 | alias Squeeze.Repo
7 |
8 | @doc false
9 | def run(_) do
10 | Mix.Task.run("app.start")
11 | # Add start date to all existing challenges
12 | Repo.all(Challenge)
13 | |> Enum.map(fn c -> Challenge.changeset(c, %{start_date: Timex.to_date(c.start_at), end_date: Timex.to_date(c.end_at)}) end)
14 | |> Enum.map(fn c -> Repo.update!(c) end)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/assets/css/core/modals/_modal.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Modal
3 | //
4 |
5 |
6 | .modal-title {
7 | font-size: $modal-title-font-size;
8 | }
9 |
10 |
11 | // Fluid modal
12 |
13 | .modal-fluid {
14 | .modal-dialog {
15 | margin-top: 0;
16 | margin-bottom: 0;
17 | }
18 |
19 | .modal-content {
20 | border-radius: 0;
21 | }
22 | }
23 |
24 |
25 | // Background color variations
26 |
27 | @each $color, $value in $theme-colors {
28 | .modal-#{$color} {
29 | @include modal-variant($value);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190318033427_create_races.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateRaces do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:races) do
6 | add :name, :string, null: false
7 | add :slug, :string, null: false
8 |
9 | add :description, :string
10 |
11 | add :city, :string
12 | add :state, :string
13 | add :country, :string
14 |
15 | add :url, :string
16 |
17 | timestamps()
18 | end
19 |
20 | create unique_index(:races, :slug)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= live_title_tag assigns[:page_title] || "Home", suffix: " · OpenPace" %>
5 | <%= render SqueezeWeb.SharedView, "head.html", assigns %>
6 |
7 |
8 |
9 |
10 | <%= render SqueezeWeb.SharedView, "flash.html", assigns %>
11 | <%= @inner_content %>
12 | <%= render SqueezeWeb.MenuView, "footer.html", assigns %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171022220412_create_credentials.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateCredentials do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:credentials) do
6 | add :provider, :string
7 | add :uid, :integer
8 | add :token, :string, size: 500
9 | add :user_id, references(:users, on_delete: :delete_all), null: false
10 |
11 | timestamps()
12 | end
13 |
14 | create index(:credentials, [:user_id])
15 | create unique_index(:credentials, [:uid, :provider])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171105203451_create_goals.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateGoals do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:goals) do
6 | add :distance, :float
7 | add :duration, :integer
8 | add :name, :string
9 | add :date, :date
10 | add :current, :boolean, default: false, null: false
11 | add :user_id, references(:users, on_delete: :delete_all), null: false
12 |
13 | timestamps()
14 | end
15 |
16 | create index(:goals, [:user_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/assets/css/core/mixins/_alert.scss:
--------------------------------------------------------------------------------
1 | @mixin alert-variant($background, $border, $color) {
2 | color: color-yiq($background);
3 | border-color: $border;
4 | @include gradient-bg($background);
5 |
6 | a {
7 | color: darken($background, 30%);
8 | font-weight: 600;
9 |
10 | &:hover {
11 | color: color-yiq($background);
12 | }
13 | }
14 |
15 | hr {
16 | border-top-color: darken($border, 5%);
17 | }
18 |
19 | .alert-link {
20 | color: darken($color, 10%);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/squeeze/races/event.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Races.Event do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 |
6 | alias Squeeze.Races.{Race}
7 |
8 | # @required_fields ~w(name slug city state country)a
9 | # @optional_fields ~w(content url)a
10 |
11 | schema "race_events" do
12 | field :name, :string
13 | field :details, :string
14 |
15 | field :start_at, :naive_datetime
16 |
17 | field :distance, :float
18 | field :distance_name, :string
19 |
20 | belongs_to :race, Race
21 |
22 | timestamps()
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190501143151_remote_activity_from_trackpoints.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.RemoteActivityFromTrackpoints do
2 | use Ecto.Migration
3 |
4 | def up do
5 | drop index(:trackpoints, [:activity_id])
6 | alter table(:trackpoints) do
7 | remove :activity_id
8 | end
9 | end
10 |
11 | def down do
12 | alter table(:trackpoints) do
13 | add :activity_id, references(:activities, on_delete: :delete_all), null: false
14 | end
15 |
16 | create index(:trackpoints, [:activity_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/squeeze/accounts/user_prefs_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Accounts.UserPrefsTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.Accounts.UserPrefs
5 |
6 | import Squeeze.Factory
7 |
8 | @valid_attrs params_for(:user_prefs)
9 |
10 | test "changeset with full attributes" do
11 | changeset = UserPrefs.changeset(%UserPrefs{}, @valid_attrs)
12 | assert changeset.valid?
13 | end
14 |
15 | test "changeset with no attributes" do
16 | changeset = UserPrefs.changeset(%UserPrefs{}, %{})
17 | assert changeset.valid?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_shadows.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Shadows
3 | //
4 |
5 | // General styles
6 | [class*="shadow"] {
7 | @if $enable-transitions {
8 | transition: $transition-base;
9 | }
10 | }
11 |
12 |
13 | // Size variations
14 | .shadow-sm--hover:hover {
15 | box-shadow: $box-shadow-sm !important;
16 | }
17 |
18 | .shadow--hover:hover {
19 | box-shadow: $box-shadow !important;
20 | }
21 |
22 | .shadow-lg--hover:hover {
23 | box-shadow: $box-shadow-lg !important;
24 | }
25 |
26 | .shadow-none--hover:hover {
27 | box-shadow: none !important;
28 | }
29 |
--------------------------------------------------------------------------------
/lib/squeeze/logger/webhook_event.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Logger.WebhookEvent do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | @moduledoc """
6 | This module contains the schema for logging webhook events.
7 | """
8 |
9 | schema "webhook_events" do
10 | field :body, :string
11 | field :provider, :string
12 | field :provider_id, :string
13 |
14 | timestamps()
15 | end
16 |
17 | @doc false
18 | def changeset(webhook_event, attrs) do
19 | webhook_event
20 | |> cast(attrs, [:provider, :provider_id, :body])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/weekly_summary_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.WeeklySummaryComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | alias Squeeze.Distances
6 |
7 | def completed_distance(%{activities: activities, current_user: user}) do
8 | activities
9 | |> Enum.map(&(&1.distance || 0))
10 | |> Enum.sum()
11 | |> Distances.to_float(imperial: user.user_prefs.imperial)
12 | end
13 |
14 | def distance_label(%{current_user: user}) do
15 | Distances.label(imperial: user.user_prefs.imperial)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/honeypot_input.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.HoneypotInput do
2 | @moduledoc false
3 |
4 | use Phoenix.HTML
5 |
6 | def honeypot_input(form, field, opts \\ []) do
7 | opts = opts ++ input_opts()
8 | [
9 | text_input(form, field, opts)
10 | ]
11 | end
12 |
13 | defp input_opts do
14 | [autocomplete: "off", tabindex: "-1", style: css_strategy()]
15 | end
16 |
17 | defp css_strategy do
18 | [
19 | "position:absolute!important;top:-9999px;left:-9999px;"
20 | ]
21 | |> Enum.random()
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190423031111_create_billing_invoices.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateBillingInvoices do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:billing_invoices) do
6 | add :name, :string
7 | add :amount_due, :integer
8 | add :status, :string
9 | add :due_date, :utc_datetime
10 | add :provider_id, :string
11 | add :user_id, references(:users, on_delete: :nothing)
12 |
13 | timestamps()
14 | end
15 |
16 | create index(:billing_invoices, [:provider_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/squeeze/accounts/credential_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Accounts.CredentialTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.Accounts.Credential
5 |
6 | import Squeeze.Factory
7 |
8 | @valid_attrs params_for(:credential)
9 |
10 | test "changeset with full attributes" do
11 | changeset = Credential.changeset(%Credential{}, @valid_attrs)
12 | assert changeset.valid?
13 | end
14 |
15 | test "changeset with no attributes" do
16 | changeset = Credential.changeset(%Credential{}, %{})
17 | refute changeset.valid?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/assets/css/pages/_namer.scss:
--------------------------------------------------------------------------------
1 | .btn-strava {
2 | width: 200px;
3 | }
4 |
5 | .namer-example-txt {
6 | position: absolute;
7 | top: 75px;
8 | left: 12px;
9 | color: $black;
10 | font-weight: 600;
11 |
12 | @include media-breakpoint-up(md) {
13 | top: 44px;
14 | font-size: 12px;
15 | left: 6px;
16 | }
17 |
18 | @include media-breakpoint-up(lg) {
19 | top: 62px;
20 | font-size: 14px;
21 | left: 8px;
22 | }
23 |
24 | @include media-breakpoint-up(xl) {
25 | top: 80px;
26 | font-size: 16px;
27 | left: 12px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/assets/js/components/avatar.js:
--------------------------------------------------------------------------------
1 | import { u } from 'umbrellajs';
2 |
3 | function init() {
4 | u('.avatar img').each((image) => {
5 | if (image.complete && image.naturalHeight === 0) {
6 | image.classList.add("d-none");
7 | }
8 | });
9 |
10 | u('.avatar img').on('load', (x) => {
11 | const image = x.target;
12 |
13 | if (image.complete && image.naturalHeight === 0) {
14 | image.classList.add("d-none");
15 | }
16 | });
17 | }
18 |
19 | window.addEventListener("load", init);
20 | window.addEventListener("phx:page-loading-stop", init);
21 |
--------------------------------------------------------------------------------
/assets/js/components/date-picker.js:
--------------------------------------------------------------------------------
1 | import flatpickr from 'flatpickr';
2 | // import rangePlugin from 'flatpickr/dist/plugins/rangePlugin';
3 |
4 | import { u } from 'umbrellajs';
5 |
6 | function load() {
7 | u('.date-picker').each((el) => {
8 | const options = {
9 | inline: el.dataset["inline"],
10 | };
11 |
12 | if (el.dataset["range"]) {
13 | options["mode"] = "range";
14 | }
15 |
16 | flatpickr(el, options);
17 | });
18 | }
19 |
20 | window.addEventListener("load", load);
21 | window.addEventListener("phx:page-loading-stop", load);
22 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/layout/live.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <%= live_component(SqueezeWeb.FlashComponent, flash: @flash) %>
3 |
4 | <%= live_component(SqueezeWeb.NavbarComponent, id: "nav-bar", current_user: @current_user) %>
5 |
6 |
7 |
8 |

9 |
10 |
11 |
12 | <%= @inner_content %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/race/_breadcrumbs.html.eex:
--------------------------------------------------------------------------------
1 |
2 | -
3 | <%= link("Home", to: Routes.home_path(@conn, :index)) %>
4 |
5 | -
6 | <%= link("Races", to: Routes.search_path(@conn, :index)) %>
7 |
8 | -
9 | <%= link(region(assigns), to: Routes.region_search_path(@conn, :index, @race.state)) %>
10 |
11 | -
12 | <%= @race.name %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190627171045_create_race_events.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateRaceEvents do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:race_events) do
6 | add :name, :string
7 | add :details, :text
8 | add :start_at, :naive_datetime
9 |
10 | add :distance, :float
11 | add :distance_name, :string
12 |
13 | add :race_id, references(:races, on_delete: :delete_all), null: false
14 |
15 | timestamps()
16 | end
17 |
18 | create index(:race_events, [:race_id])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/coveralls.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_stop_words": [
3 | "defmodule",
4 | "defrecord",
5 | "defimpl",
6 | "defexception",
7 | "defprotocol",
8 | "defstruct",
9 | "def.+(.+\\\\.+).+do",
10 | "^\\s+use\\s+"
11 | ],
12 |
13 | "custom_stop_words": [
14 | ],
15 |
16 | "coverage_options": {
17 | "treat_no_relevant_lines_as_covered": false,
18 | "output_dir": "cover/",
19 | "minimum_coverage": 0
20 | },
21 |
22 | "skip_files": [
23 | "test"
24 | ],
25 |
26 | "terminal_options": {
27 | "file_column_width": 40
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/squeeze/namer/duration_formatter.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Namer.DurationFormatter do
2 | @moduledoc """
3 | This module formats the duration
4 | """
5 |
6 | def format(%{distance: distance}) when distance > 0, do: nil
7 | def format(%{moving_time: t}) do
8 | minutes = trunc(rem(t, 60 * 60) / 60)
9 | hours = trunc(t / (60 * 60))
10 |
11 | if hours > 0 do
12 | "#{hours}h #{pad_num(minutes)}m"
13 | else
14 | "#{minutes}min"
15 | end
16 | end
17 |
18 | defp pad_num(x) when x < 10, do: "0#{x}"
19 | defp pad_num(x), do: "#{x}"
20 | end
21 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/api/push_token_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.PushTokenController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.Notifications
6 |
7 | action_fallback SqueezeWeb.Api.FallbackController
8 |
9 | def create(conn, %{"token" => token}) do
10 | user = conn.assigns.current_user
11 | with {:ok, push_token} <- Notifications.create_push_token(user, token) do
12 | conn
13 | |> put_status(:created)
14 | |> render("create.json", %{push_token: push_token})
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20181223194415_add_default_fields_to_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDefaultFieldsToActivities do
2 | use Ecto.Migration
3 |
4 | def up do
5 | alter table(:activities) do
6 | modify :distance, :float, default: 0.0, null: false
7 | modify :duration, :integer, default: 0, null: false
8 | end
9 | end
10 |
11 | def down do
12 | alter table(:activities) do
13 | modify :distance, :float, default: nil, null: true
14 | modify :duration, :integer, default: nil, null: true
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211106190100_add_default_timeline_to_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDefaultTimelineToChallenges do
2 | use Ecto.Migration
3 |
4 | alias Squeeze.Challenges.Challenge
5 |
6 | def up do
7 | statuses = Ecto.Enum.mappings(Challenge, :timeline)
8 | alter table(:challenges) do
9 | modify :timeline, :integer, default: statuses[:custom], null: false
10 | end
11 | end
12 |
13 | def down do
14 | alter table(:users) do
15 | modify :timeline, :integer, default: nil, null: true
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/support/mocks.ex:
--------------------------------------------------------------------------------
1 | # Notification Mock
2 | Mox.defmock(Squeeze.ExpoNotifications.MockNotificationProvider, for: Squeeze.ExpoNotifications.NotificationProvider)
3 |
4 | # Strava Mocks
5 | Mox.defmock(Squeeze.Strava.MockActivities, for: Strava.ActivitiesBehavior)
6 | Mox.defmock(Squeeze.Strava.MockAuth, for: Strava.AuthBehavior)
7 | Mox.defmock(Squeeze.Strava.MockClient, for: Strava.ClientBehavior)
8 | Mox.defmock(Squeeze.Strava.MockStreams, for: Strava.StreamsBehavior)
9 |
10 | # Payment Processor Mock
11 | Mox.defmock(Squeeze.MockPaymentProcessor, for: Squeeze.PaymentProcessor)
12 |
--------------------------------------------------------------------------------
/assets/css/core/cards/_card-pricing.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Pricing card
3 | //
4 |
5 | .card-pricing {
6 | .card-header {
7 | padding-top: 1.25rem;
8 | padding-bottom: 1.25rem;
9 | }
10 | .list-unstyled li {
11 | padding: .5rem 0;
12 | color: $gray-600;
13 | }
14 | }
15 |
16 | .card-pricing.popular {
17 | z-index: 1;
18 | border: 3px solid theme-color("primary") !important;
19 | }
20 |
21 | @include media-breakpoint-up(md) {
22 | .card-pricing.zoom-in {
23 | z-index: 1;
24 | transform: scale(1.1);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/assets/js/components/btn-spinner.js:
--------------------------------------------------------------------------------
1 | import { u } from 'umbrellajs';
2 |
3 | u(document).on('submit', 'form', function(event) {
4 | const $form = u(this);
5 | const $button = $form.find('.btn-spinner');
6 | if ($button.length === 0) {
7 | return;
8 | }
9 |
10 | const width = $button.size().width;
11 | $button.attr('disabled', true);
12 | $button.attr('style', `width: ${width}px`);
13 | $button.html(`
14 |
15 | Loading...
16 | `);
17 | });
18 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/dashboard/load_history_component.html.heex:
--------------------------------------------------------------------------------
1 | <%= if show_component?(assigns) do %>
2 |
3 |
4 |
Load Strava History
5 |
6 |
7 | Import all of your runs and activities from Strava to OpenPace.
8 |
9 |
10 |
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/challenge_share_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ChallengeShareView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def remaining_percentage(%{challenge: challenge}) do
6 | now = DateTime.utc_now()
7 | start_date = challenge.start_date
8 | end_date = challenge.end_date
9 |
10 | cond do
11 | Timex.diff(now, start_date) < 0 -> 0.0
12 | Timex.diff(now, end_date) > 0 -> 1 * 100.0
13 | true ->
14 | Timex.diff(now, start_date, :seconds) / Timex.diff(end_date, start_date, :seconds) * 100.0
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190425025635_add_default_subscription_status.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDefaultSubscriptionStatus do
2 | use Ecto.Migration
3 |
4 | alias Squeeze.Accounts.User
5 |
6 | def up do
7 | statuses = Ecto.Enum.mappings(User, :subscription_status)
8 | alter table(:users) do
9 | modify :subscription_status, :integer, default: statuses[:active], null: false
10 | end
11 | end
12 |
13 | def down do
14 | alter table(:users) do
15 | modify :subscription_status, :integer, default: nil, null: true
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/squeeze/billing/payment_method_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Accounts.PaymentMethodTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.Billing.PaymentMethod
5 |
6 | import Squeeze.Factory
7 |
8 | @valid_attrs params_for(:payment_method)
9 |
10 | test "changeset with full attributes" do
11 | changeset = PaymentMethod.changeset(%PaymentMethod{}, @valid_attrs)
12 | assert changeset.valid?
13 | end
14 |
15 | test "changeset with no attributes" do
16 | changeset = PaymentMethod.changeset(%PaymentMethod{}, %{})
17 | refute changeset.valid?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/squeeze/notifications/push_token.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Notifications.PushToken do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 | alias Squeeze.Accounts.{User}
7 |
8 | schema "push_tokens" do
9 | field :token, :string
10 | belongs_to :user, User
11 |
12 | timestamps()
13 | end
14 |
15 | @doc false
16 | def changeset(push_token, attrs) do
17 | push_token
18 | |> cast(attrs, [:token])
19 | |> validate_required([:token])
20 | |> unique_constraint(:unique_token, name: :push_tokens_user_id_token_index)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190102231358_add_default_timezone_user_prefs.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.AddDefaultTimezoneUserPrefs do
2 | use Ecto.Migration
3 |
4 | def up do
5 | execute """
6 | UPDATE user_prefs SET timezone = 'America/New_York' WHERE timezone IS NULL;
7 | """
8 |
9 | alter table(:user_prefs) do
10 | modify :timezone, :string, default: "America/New_York", null: false
11 | end
12 | end
13 |
14 | def down do
15 | alter table(:user_prefs) do
16 | modify :timezone, :string, default: nil, null: true
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/garmin_webhook_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.GarminWebhookControllerTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | describe "GET /webhook" do
5 | test "returns 200 and empty response", %{conn: conn} do
6 | conn = get(conn, "/webhook/garmin")
7 | assert json_response(conn, 200) == %{}
8 | end
9 | end
10 |
11 | describe "POST /webhook" do
12 | test "returns 200 and empty response", %{conn: conn} do
13 | conn = post(conn, "/webhook/garmin")
14 | assert json_response(conn, 200) == %{}
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210307043053_create_challenge_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateChallengeActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:challenge_activities) do
6 | add :amount, :float, null: false
7 |
8 | add :challenge_id, references(:challenges, on_delete: :delete_all), null: false
9 | add :activity_id, references(:activities, on_delete: :delete_all), null: false
10 |
11 | timestamps()
12 | end
13 |
14 | create unique_index(:challenge_activities, [:challenge_id, :activity_id])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/assets/css/core/badges/_badge-circle.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Circle badge
3 | //
4 |
5 |
6 | // General styles
7 |
8 | .badge-circle {
9 | text-align: center;
10 | display: inline-flex;
11 | align-items: center;
12 | justify-content: center;
13 | border-radius: 50%;
14 | padding: 0 !important;
15 | width: 1.25rem;
16 | height: 1.25rem;
17 | font-size: .75rem;
18 | font-weight: 600;
19 |
20 | &.badge-md {
21 | width: 1.5rem;
22 | height: 1.5rem;
23 | }
24 |
25 | &.badge-lg {
26 | width: 2rem;
27 | height: 2rem;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/distance_search_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.DistanceSearchController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.RaceSearch
6 |
7 | def index(conn, %{"region" => region, "distance" => distance}) do
8 | case RaceSearch.search_distance_region(%{distance: distance, region: region}) do
9 | {:ok, results} ->
10 | render(conn, "index.html", region: region, distance: distance, results: results)
11 | _ ->
12 | render(conn, "index.html", region: region, distance: distance)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/region_search_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.RegionSearchController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.RaceSearch
6 | alias Squeeze.Regions
7 |
8 | def index(conn, %{"region" => slug}) do
9 | region = Regions.from_slug(slug)
10 |
11 | case RaceSearch.search_region(%{region: region.long_name}) do
12 | {:ok, results} ->
13 | render(conn, "index.html", region: region, results: results)
14 | _ ->
15 | render(conn, "index.html", region: region, results: [])
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190224020703_create_trackpoints.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateTrackpoints do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:trackpoints) do
6 | add :altitude, :float
7 | add :distance, :float
8 | add :heartrate, :integer
9 | add :velocity, :float
10 | add :cadence, :integer
11 | add :coordinates, :map
12 | add :time, :integer
13 | add :activity_id, references(:activities, on_delete: :delete_all), null: false
14 | end
15 |
16 | create index(:trackpoints, [:activity_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/assets/css/core/shortcuts/_shortcut.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Shortcut
3 | //
4 |
5 | .shortcuts {
6 |
7 | }
8 |
9 | .shortcut-media {
10 | @include transition($transition-cubic-bezier);
11 | }
12 |
13 | .shortcut-item {
14 | padding-top: 1rem;
15 | padding-bottom: 1rem;
16 | text-align: center;
17 |
18 | small {
19 | display: block;
20 | margin-top: .75rem;
21 | font-size: $h5-font-size;
22 | font-weight: $heading-font-weight;
23 | }
24 |
25 | &:hover {
26 | .shortcut-media {
27 | transform: scale(1.1);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/squeeze/training_plans/event.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.TrainingPlans.Event do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 |
6 | alias Squeeze.TrainingPlans.{Plan}
7 |
8 | schema "training_events" do
9 | field :name, :string
10 | field :distance, :float
11 | field :duration, :integer
12 | field :type, :string
13 |
14 | field :warmup, :boolean
15 | field :cooldown, :boolean
16 |
17 | field :plan_position, :integer
18 | field :day_position, :integer
19 |
20 | belongs_to :plan, Plan, foreign_key: :training_plan_id
21 |
22 | timestamps()
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/shared/_gtm.html.eex:
--------------------------------------------------------------------------------
1 | <% gtm_id = Application.get_env(:squeeze, :gtm_id) %>
2 |
3 | <%= if gtm_id && gtm_id != "" do %>
4 |
5 |
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/distance_search_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.DistanceSearchView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def title(_page, assigns) do
6 | "Best races in #{region_name(assigns)}"
7 | end
8 |
9 | def region_name(%{region: region}) do
10 | String.capitalize(region)
11 | end
12 |
13 | def distance_name(%{distance: distance}) do
14 | distance
15 | |> String.split("-")
16 | |> Enum.map_join(" ", &String.capitalize/1)
17 | end
18 |
19 | def h1(assigns) do
20 | "#{distance_name(assigns)} races in #{region_name(assigns)}"
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180817035931_change_user_prefs_reference_to_users.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.ChangeUserPrefsReferenceToUsers do
2 | use Ecto.Migration
3 |
4 | def up do
5 | drop constraint(:user_prefs, "user_prefs_user_id_fkey")
6 | alter table(:user_prefs) do
7 | modify :user_id, references(:users, on_delete: :delete_all), null: false
8 | end
9 | end
10 |
11 | def down do
12 | drop constraint(:user_prefs, "user_prefs_user_id_fkey")
13 | alter table(:user_prefs) do
14 | modify :user_id, references(:users, on_delete: :nothing)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211216185228_create_race_goals.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateRaceGoals do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:race_goals) do
6 | add :duration, :integer
7 | add :just_finish, :boolean, default: false, null: false
8 |
9 | add :user_id, references(:users, on_delete: :delete_all), null: false
10 | add :race_id, references(:races, on_delete: :nothing), null: false
11 |
12 | timestamps()
13 | end
14 |
15 | create index(:race_goals, [:user_id])
16 | create index(:race_goals, [:race_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/assets/css/core/vendors/_headroom.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Headroom
3 | //
4 |
5 |
6 | .headroom {
7 | will-change: transform;
8 | background-color: inherit;
9 | @include transition($transition-base);
10 | }
11 | .headroom--pinned {
12 | @extend .position-fixed;
13 | transform: translateY(0%);
14 | }
15 | .headroom--unpinned {
16 | @extend .position-fixed;
17 | transform: translateY(-100%);
18 | }
19 |
20 | .headroom--not-top {
21 | padding-top: .5rem;
22 | padding-bottom: .5rem;
23 | background-color: theme-color("default") !important;
24 | box-shadow: 0 1px 10px rgba(130, 130, 134, 0.1);
25 | }
26 |
--------------------------------------------------------------------------------
/assets/js/components/copy-input.js:
--------------------------------------------------------------------------------
1 | import { u } from 'umbrellajs';
2 |
3 | function init() {
4 | const $input = u('.copy-input input');
5 |
6 | $input.on("click", (e) => {
7 | const node = u(e.target).parent('.copy-input').find('input').first();
8 |
9 | /* Select the text field */
10 | node.select();
11 | node.setSelectionRange(0, 99999); /* For mobile devices */
12 |
13 | /* Copy the text inside the text field */
14 | navigator.clipboard.writeText(node.value);
15 | });
16 | };
17 |
18 | window.addEventListener("phx:page-loading-stop", init);
19 | window.addEventListener("load", init);
20 |
--------------------------------------------------------------------------------
/lib/squeeze/namer/relative_time_formatter.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Namer.RelativeTimeFormatter do
2 | @moduledoc """
3 | This module converts an activity into Morning, Afternoon, Evening, and Night
4 | """
5 |
6 | def format(%{start_date_local: timestamp}) when not is_nil(timestamp) do
7 | case timestamp.hour do
8 | x when x >= 5 and x < 12 -> "Morning" # 5am until 12 noon
9 | x when x >= 12 and x < 17 -> "Afternoon" # noon until 5pm
10 | x when x >= 17 and x < 21 -> "Evening" # 5pm until 9pm
11 | _ -> "Night" # After 9pm
12 | end
13 | end
14 |
15 | def format(_), do: nil
16 | end
17 |
--------------------------------------------------------------------------------
/lib/squeeze_web/plugs/require_registered.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Plug.RequireRegistered do
2 | import Plug.Conn
3 |
4 | @moduledoc """
5 | This modules requires either:
6 |
7 | 1. The user has registered
8 | 2. The page has a query param of welcome
9 | """
10 |
11 | alias Phoenix.Controller
12 |
13 | def init(_), do: nil
14 |
15 | def call(conn, _x) do
16 | user = conn.assigns.current_user
17 |
18 | if user || !is_nil(conn.query_params["welcome"]) do
19 | conn
20 | else
21 | conn
22 | |> Controller.redirect(to: "/")
23 | |> halt()
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200907210117_create_challenges.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateChallenges do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:challenges) do
6 | add :name, :string
7 | add :start_at, :naive_datetime
8 | add :end_at, :naive_datetime
9 | add :activity_type, :integer
10 | add :challenge_type, :integer
11 | add :timeline, :integer
12 |
13 | add :user_id, references(:users, on_delete: :delete_all), null: false
14 |
15 | timestamps()
16 | end
17 |
18 | create index(:challenges, [:user_id])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/assets/css/core/masks/_mask.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Mask
3 | //
4 |
5 | .mask {
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | height: 100%;
11 | @include transition($transition-base);
12 | }
13 |
14 |
15 | // Backdrop
16 |
17 | .backdrop {
18 | position: fixed;
19 | top: 0;
20 | left: 0;
21 | width: 100%;
22 | height: 100%;
23 | cursor: pointer;
24 | z-index: 1040; // navbar fixed has a z-index of 1030
25 | }
26 |
27 | .backdrop-dark {
28 | background: rgba($black, .3);
29 | }
30 |
31 | .backdrop-light {
32 | background: rgba($white, .3);
33 | }
34 |
--------------------------------------------------------------------------------
/assets/css/core/navs/_nav.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Nav
3 | //
4 |
5 |
6 | // Nav wrapper (container)
7 |
8 | // Nav wrapper
9 | .nav-wrapper {
10 | padding: 1rem 0;
11 | @include border-top-radius($card-border-radius);
12 |
13 | + .card {
14 | @include border-top-radius(0);
15 | @include border-bottom-radius($card-border-radius);
16 | }
17 | }
18 |
19 |
20 | // Nav links
21 |
22 | .nav-link {
23 | color: $nav-link-color;
24 |
25 | &:hover {
26 | color: $nav-link-hover-color;
27 | }
28 |
29 | i.ni {
30 | position: relative;
31 | top: 2px;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/assets/css/core/vendors/_jvectormap.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Jvector Map
3 | //
4 |
5 |
6 | .vector-map {
7 | position: relative;
8 | height: 600px;
9 | }
10 |
11 |
12 | // Size variations
13 |
14 | .vector-map-sm {
15 | height: 280px;
16 | }
17 |
18 |
19 | // Vendor overrides
20 |
21 | .jvectormap-container {
22 | width: 100%;
23 | height: 100%;
24 | }
25 |
26 | .jvectormap-zoomin,
27 | .jvectormap-zoomout {
28 | position: absolute;
29 | left: 0;
30 | bottom: 0;
31 | }
32 |
33 | .jvectormap-zoomin {
34 | bottom: 4.25rem;
35 | }
36 |
37 | .jvectormap-zoomout {
38 | bottom: 2rem;
39 | }
40 |
--------------------------------------------------------------------------------
/lib/squeeze/payment_processor/payment_processor.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.PaymentProcessor do
2 | @moduledoc false
3 |
4 | @callback create_card(map()) :: {:ok, struct()} | {:error, struct()}
5 | @callback create_customer(map()) :: {:ok, struct()} | {:error, struct()}
6 | @callback create_plan(map()) :: {:ok, struct()} | {:error, struct()}
7 | @callback create_product(map()) :: {:ok, struct()} | {:error, struct()}
8 | @callback create_subscription(String.t, String.t, integer) ::
9 | {:ok, struct()} | {:error, struct()}
10 | @callback cancel_subscription(String.t) :: {:ok, struct()} | {:error, struct()}
11 | end
12 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/race/_hero.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= @race.name %>
5 |
6 |
7 | <%= if start_at(assigns) do %>
8 | <%= date(assigns) %> | <%= time(assigns) %> |
9 | <% end %>
10 | <%= location(assigns) %>
11 | <%= if @race.url do %>
12 | | Official Race Website
13 | <% end %>
14 |
15 |
16 |
17 | Learn more »
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/squeeze/velocity_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.VelocityTest do
2 | use Squeeze.DataCase
3 |
4 | @moduledoc false
5 |
6 | alias Squeeze.Velocity
7 |
8 | describe "to_float/2" do
9 | test "when velocity is zero, it returns zero" do
10 | assert Velocity.to_float(0.0) == 0.0
11 | end
12 |
13 | test "when imperial: true, it returns minutes per mile" do
14 | assert Velocity.to_float(3.4, imperial: true) == 7.89
15 | end
16 |
17 | test "when imperial: false, it returns minutes per kilometer" do
18 | assert Velocity.to_float(3.4, imperial: false) == 4.9
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200912221618_create_scores.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateScores do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:scores) do
6 | add :score, :integer, null: false, default: 0
7 | add :user_id, references(:users, on_delete: :delete_all), null: false
8 | add :challenge_id, references(:challenges, on_delete: :delete_all), null: false
9 |
10 | timestamps()
11 | end
12 |
13 | create index(:scores, [:user_id])
14 | create index(:scores, [:challenge_id])
15 |
16 | create unique_index(:scores, [:user_id, :challenge_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/challenges/static_map_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Challenges.StaticMapComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | alias SqueezeWeb.MapboxStaticMap
6 |
7 | def map_url(polyline) do
8 | opts = [
9 | height: 300,
10 | width: 500,
11 | show_pins: true,
12 | outline_color: "#FFFFFF"
13 | ]
14 | MapboxStaticMap.map_url(polyline, opts)
15 | end
16 |
17 | def description(user, segment) do
18 | distance = format_distance(segment.distance, user.user_prefs)
19 | "#{distance} - #{segment.city}, #{segment.state}"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/squeeze/races/result_summary.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Races.ResultSummary do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 |
6 | alias Squeeze.Duration
7 | alias Squeeze.Races.{Race}
8 |
9 | schema "race_result_summaries" do
10 | field :distance, :float
11 | field :distance_name, :string
12 | field :start_date, :date
13 |
14 | field :finisher_count, :integer
15 |
16 | field :male_winner_time, Duration
17 | field :female_winner_time, Duration
18 | field :male_avg_time, Duration
19 | field :female_avg_time, Duration
20 |
21 | belongs_to :race, Race
22 |
23 | timestamps()
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/squeeze/logger/logger_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.LoggerTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.Logger
5 |
6 | describe "webhook_events" do
7 | alias Squeeze.Logger.WebhookEvent
8 |
9 | test "log_webhook_event/1 creates a webhook_event" do
10 | attrs = %{body: "body", provider: "stripe", provider_id: "event_123456789"}
11 | assert {:ok, %WebhookEvent{} = webhook_event} = Logger.log_webhook_event(attrs)
12 | assert webhook_event.body == "body"
13 | assert webhook_event.provider == "stripe"
14 | assert webhook_event.provider_id == "event_123456789"
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/mix/tasks/setup.stripe.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Setup.Stripe do
2 | use Mix.Task
3 |
4 | @moduledoc """
5 | Creates the required stripe billing setup. This task must be run for both
6 | test and live stripe environment keys.
7 |
8 | * Creates a product
9 | * Creates a plan for $5.95 a month
10 |
11 | ## Examples
12 |
13 | cmd> mix setup.stripe
14 | Created stripe product prod_EOFa0u5fMBxRqd
15 | Created stripe plan plan_EOxadIoraD2MOx
16 | """
17 |
18 | alias Squeeze.Setup.StripeSetup
19 |
20 | @doc false
21 | def run(_) do
22 | Mix.Task.run("app.start")
23 | StripeSetup.setup()
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/fitbit_webhook_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.FitbitWebhookControllerTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | describe "GET /webhook" do
5 | test "verify with correct verify_token", %{conn: conn} do
6 | conn = conn
7 | |> get(fitbit_webhook_path(conn, :webhook), verify: "FITBIT")
8 |
9 | assert response(conn, 204) == ""
10 | end
11 |
12 | test "verify with incorrect verify_token", %{conn: conn} do
13 | conn = conn
14 | |> get(fitbit_webhook_path(conn, :webhook), verify: "1234")
15 |
16 | assert json_response(conn, 404) == %{}
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.ErrorView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | # If you want to customize a particular status code
6 | # for a certain format, you may uncomment below.
7 | # def render("500.json", _assigns) do
8 | # %{errors: %{detail: "Internal Server Error"}}
9 | # end
10 |
11 | # By default, Phoenix returns the status message from
12 | # the template name. For example, "404.json" becomes
13 | # "Not Found".
14 | def template_not_found(template, _assigns) do
15 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/factories/invoice_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.InvoiceFactory do
2 | @moduledoc false
3 |
4 | alias Faker.{Lorem}
5 | alias Squeeze.Billing.Invoice
6 |
7 | defmacro __using__(_opts) do
8 | quote do
9 | def invoice_factory do
10 | date = Faker.DateTime.forward(10)
11 |
12 | %Invoice{
13 | name: "#{DateTime.to_date(date)} Invoice",
14 | amount_due: 595,
15 | provider_id: "invoice_#{Lorem.characters(15)}",
16 | status: Enum.random(["pending", "paid"]),
17 | due_date: date,
18 |
19 | user: build(:user)
20 | }
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/squeeze/logger/logger.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Logger do
2 | @moduledoc """
3 | The Logger context.
4 | """
5 |
6 | import Ecto.Query, warn: false
7 | alias Squeeze.Repo
8 |
9 | alias Squeeze.Logger.WebhookEvent
10 |
11 | @doc """
12 | Creates a webhook_event.
13 |
14 | ## Examples
15 |
16 | iex> log_webhook_event(%{field: value})
17 | {:ok, %WebhookEvent{}}
18 |
19 | iex> log_webhook_event(%{field: bad_value})
20 | {:error, %Ecto.Changeset{}}
21 |
22 | """
23 | def log_webhook_event(attrs \\ %{}) do
24 | %WebhookEvent{}
25 | |> WebhookEvent.changeset(attrs)
26 | |> Repo.insert()
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/squeeze/race_search.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.RaceSearch do
2 | @moduledoc false
3 |
4 | @index_name "Race"
5 | @facets ~w(full_state weekday month course_profile course_terrain course_type boston_qualifier)
6 |
7 | def search do
8 | Algolia.search(@index_name, "", [facets: @facets])
9 | end
10 |
11 | def search_region(%{region: region}) do
12 | Algolia.search(@index_name, "", [facetFilters: ["full_state:#{region}"], facets: @facets])
13 | end
14 |
15 | def search_distance_region(%{distance: _distance, region: region}) do
16 | Algolia.search(@index_name, "", [facetFilters: ["full_state:#{region}"], facets: @facets])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210729181828_create_follows.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateFollows do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:follows) do
6 | add :follower_id, references(:users, on_delete: :delete_all)
7 | add :followee_id, references(:users, on_delete: :delete_all)
8 | add :pending, :boolean, null: false, default: false
9 |
10 | timestamps()
11 | end
12 |
13 | create index(:follows, [:follower_id])
14 | create index(:follows, [:followee_id])
15 |
16 | # Restrict duplicate follows
17 | create unique_index(:follows, [:follower_id, :followee_id])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211229174335_default_to_free_subscription.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.DefaultToFreeSubscription do
2 | use Ecto.Migration
3 |
4 | alias Squeeze.Accounts.User
5 |
6 | def up do
7 | statuses = Ecto.Enum.mappings(User, :subscription_status)
8 | alter table(:users) do
9 | modify :subscription_status, :integer, default: statuses[:free], null: false
10 | end
11 | end
12 |
13 | def down do
14 | statuses = Ecto.Enum.mappings(User, :subscription_status)
15 | alter table(:users) do
16 | modify :subscription_status, :integer, default: statuses[:active], null: false
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/assets/css/core/cards/_card-money.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Money card
3 | // A visual representation of a credit/debit card
4 | //
5 |
6 | .card-serial-number {
7 | display: flex;
8 | justify-content: space-between;
9 | font-size: $h1-font-size;
10 |
11 | > div:not(:last-child) {
12 | display: flex;
13 | flex: 1 1 auto;
14 |
15 | &:after {
16 | content: "-";
17 | flex: 1 1 auto;
18 | text-align: center;
19 | position: relative;
20 | left: -2px;
21 | }
22 | }
23 |
24 | @include media-breakpoint-down(xs) {
25 | font-size: $h3-font-size;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200912194830_create_user_challenge.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateUserChallenge do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:user_challenge, primary_key: false) do
6 | add :user_id, references(:users, on_delete: :delete_all), primary_key: true
7 | add :challenge_id, references(:challenges, on_delete: :delete_all), primary_key: true
8 | end
9 |
10 | create index(:user_challenge, [:user_id])
11 | create index(:user_challenge, [:challenge_id])
12 |
13 | create unique_index(:user_challenge, [:user_id, :challenge_id], name: :user_id_challenge_id_unique_index)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/squeeze/dashboard/trackpoint_set.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Dashboard.TrackpointSet do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 |
7 | alias Squeeze.Dashboard.{Activity, Trackpoint}
8 |
9 | @required_fields ~w()a
10 | @optional_fields ~w()a
11 |
12 | schema "trackpoint_sets" do
13 | belongs_to :activity, Activity
14 |
15 | embeds_many :trackpoints, Trackpoint
16 |
17 | timestamps()
18 | end
19 |
20 | @doc false
21 | def changeset(trackpoint, attrs \\ %{}) do
22 | trackpoint
23 | |> cast(attrs, @required_fields ++ @optional_fields)
24 | |> validate_required(@required_fields)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/squeeze/strava/activities.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Strava.Activities do
2 | @moduledoc """
3 | Wrapper around Strava.Activities
4 | """
5 |
6 | alias Squeeze.Accounts.User
7 | alias Squeeze.Strava.Client
8 | alias Strava.DetailedActivity
9 |
10 | import Strava.RequestBuilder
11 |
12 | def get_activity_by_id(%User{} = user, id) do
13 | user
14 | |> Client.new()
15 | |> Strava.Activities.get_activity_by_id(id)
16 | end
17 |
18 | def update_activity_by_id(user, activity_id, attrs) do
19 | user
20 | |> Client.new()
21 | |> Strava.Client.put("/activities/#{activity_id}", attrs)
22 | |> decode(%DetailedActivity{})
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/race/_events.html.eex:
--------------------------------------------------------------------------------
1 | <%= if length(@race.events) > 0 do %>
2 |
3 |
4 |
5 | |
6 | Event
7 | |
8 |
9 | Distance
10 | |
11 |
12 | Start Time
13 | |
14 |
15 |
16 |
17 |
18 | <%= for event <- @race.events do %>
19 |
20 | | <%= event.name %> |
21 | |
22 | |
23 |
24 | <% end %>
25 |
26 |
27 | <% end %>
28 |
--------------------------------------------------------------------------------
/test/factories/race_event_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.RaceEventFactory do
2 | @moduledoc false
3 |
4 | alias Faker.{Lorem, NaiveDateTime}
5 | alias Squeeze.Distances
6 | alias Squeeze.Races.Event
7 |
8 | defmacro __using__(_opts) do
9 | quote do
10 | def race_event_factory do
11 | race = Enum.random(Distances.distances)
12 |
13 | %Event{
14 | name: race.name,
15 | details: Lorem.paragraph(1),
16 | start_at: NaiveDateTime.forward(100),
17 | distance: race.distance,
18 | distance_name: race.name,
19 | race: build(:race)
20 | }
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/assets/css/custom/_utilities.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Utilities
3 | //
4 |
5 |
6 | @import "../core/utilities/backgrounds";
7 |
8 | @import "../core/utilities/blurable";
9 |
10 | @import "../core/utilities/floating";
11 |
12 | @import "../core/utilities/helper";
13 |
14 | @import "../core/utilities/image";
15 |
16 | @import "../core/utilities/opacity";
17 |
18 | @import "../core/utilities/overflow";
19 |
20 | @import "../core/utilities/position";
21 |
22 | @import "../core/utilities/shadows";
23 |
24 | @import "../core/utilities/sizing";
25 |
26 | @import "../core/utilities/spacing";
27 |
28 | @import "../core/utilities/text";
29 |
30 | @import "../core/utilities/transform";
31 |
32 |
--------------------------------------------------------------------------------
/lib/squeeze/billing/plan.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Billing.Plan do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 |
7 | @required_fields ~w(name amount provider_id interval)a
8 | @optional_fields ~w(default)a
9 |
10 | schema "billing_plans" do
11 | field :name, :string
12 | field :amount, :integer
13 | field :provider_id, :string
14 | field :interval, :string
15 | field :default, :boolean
16 |
17 | timestamps()
18 | end
19 |
20 | @doc false
21 | def changeset(plan, attrs) do
22 | plan
23 | |> cast(attrs, @required_fields ++ @optional_fields)
24 | |> validate_required(@required_fields)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/activities/map_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | <%= if show_pace?(assigns) do %>
8 |
9 |
10 | <% pace = pace(assigns) %>
11 | <%= for color <- gradient() do %>
12 |
13 | <%= format_duration(pace / color.factor) %>
14 |
15 | <% end %>
16 |
17 |
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/test/support/factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Factory do
2 | use ExMachina.Ecto, repo: Squeeze.Repo
3 |
4 | use Squeeze.ActivityFactory
5 | use Squeeze.BillingPlanFactory
6 | use Squeeze.ChallengeFactory
7 | use Squeeze.CredentialFactory
8 | use Squeeze.DetailedActivityFactory
9 | use Squeeze.FollowFactory
10 | use Squeeze.InvoiceFactory
11 | use Squeeze.PaymentMethodFactory
12 | use Squeeze.PushTokenFactory
13 | use Squeeze.TrainingPlanFactory
14 | use Squeeze.RaceFactory
15 | use Squeeze.RaceEventFactory
16 | use Squeeze.RaceGoalFactory
17 | use Squeeze.ScoreFactory
18 | use Squeeze.UserFactory
19 | use Squeeze.UserPrefsFactory
20 |
21 | @moduledoc false
22 | end
23 |
--------------------------------------------------------------------------------
/assets/css/core/medias/_media-comment.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Media comment
3 | //
4 |
5 |
6 | .media-comment {
7 | margin-top: 2rem;
8 | }
9 |
10 | .media-comment-avatar {
11 | margin-top: -1rem;
12 | margin-right: -2rem;
13 | position: relative;
14 | z-index: 1;
15 | border: 4px solid $white;
16 | @include transition($transition-base);
17 | }
18 |
19 | .media-comment-text {
20 | border-radius: $border-radius-lg;
21 | border-top-left-radius: 0;
22 | position: relative;
23 | background-color: $gray-100;
24 | padding: 1rem 1.25rem 1rem 2.5rem;
25 | }
26 |
27 | .media-comment {
28 | &:hover {
29 | .media-comment-avatar {
30 | transform: scale(1.1);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/assets/css/core/navbars/_navbar-collapse.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Navabar collapse
3 | //
4 |
5 | // Collapse
6 |
7 | .navbar-collapse-header {
8 | display: none;
9 | }
10 |
11 |
12 |
13 | @keyframes show-navbar-collapse {
14 | 0% {
15 | opacity: 0;
16 | transform: scale(.95);
17 | transform-origin: 100% 0;
18 | }
19 |
20 | 100% {
21 | opacity: 1;
22 | transform: scale(1);
23 | }
24 | }
25 |
26 | @keyframes hide-navbar-collapse {
27 | from {
28 | opacity: 1;
29 | transform: scale(1);
30 | transform-origin: 100% 0;
31 | }
32 |
33 | to {
34 | opacity: 0;
35 | transform: scale(.95);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/challenges/podium_item_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | <%= live_redirect @challenge.name, to: Routes.challenge_path(@socket, :show, @challenge.slug) %>
11 |
12 |
13 | <%= podium_finish(assigns) %> ·
14 | <%= challenge_type(assigns) %> ·
15 | <%= challenge_relative_date(assigns) %>
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/changeset_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.ChangesetView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | @doc """
6 | Traverses and translates changeset errors.
7 |
8 | See `Ecto.Changeset.traverse_errors/2` and
9 | `SqueezeWeb.ErrorHelpers.translate_error/1` for more details.
10 | """
11 | def translate_errors(changeset) do
12 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
13 | end
14 |
15 | def render("error.json", %{changeset: changeset}) do
16 | # When encoded, the changeset returns its errors
17 | # as a JSON object. So we just pass it forward.
18 | %{errors: translate_errors(changeset)}
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/squeeze_web/controllers/challenge_share_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ChallengeShareControllerTest do
2 | use SqueezeWeb.ConnCase
3 |
4 | @tag :no_user
5 | describe "#show" do
6 | test "includes the challenge name", %{conn: conn} do
7 | challenge = insert(:challenge)
8 | conn = get(conn, "/invite/#{challenge.slug}")
9 |
10 | assert html_response(conn, 200) =~ challenge.name
11 | end
12 |
13 | test "includes a button to join challenge", %{conn: conn} do
14 | challenge = insert(:challenge)
15 | conn = get(conn, "/invite/#{challenge.slug}")
16 |
17 | assert html_response(conn, 200) =~ "Join Challenge"
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/assets/webp.js:
--------------------------------------------------------------------------------
1 | var imagemin = require("imagemin"), // The imagemin module.
2 | webp = require("imagemin-webp"), // imagemin's WebP plugin.
3 | baseFolder = "./static/images", // Change this line
4 | outputFolder = baseFolder, // Output folder
5 | PNGImages = `${baseFolder}/*.png`, // PNG images
6 | JPEGImages = `${baseFolder}/*.jpg`; // JPEG images
7 |
8 | imagemin([PNGImages], outputFolder, {
9 | plugins: [webp({
10 | lossless: true // Losslessly encode images
11 | })]
12 | });
13 |
14 | imagemin([JPEGImages], outputFolder, {
15 | plugins: [webp({
16 | quality: 65 // Quality setting from 0 to 100
17 | })]
18 | });
19 |
--------------------------------------------------------------------------------
/lib/squeeze/reporter.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Reporter do
2 | @moduledoc """
3 | This module reports information to slack
4 | """
5 | require Logger
6 |
7 | alias Slack.Web.Chat
8 | alias Squeeze.Accounts.User
9 |
10 | # @slack_token Application.compile_env(:slack, :api_token)
11 |
12 | def report_new_user(%User{} = user) do
13 | text = if user.user_prefs.rename_activities do
14 | "Namer Sign Up: #{user.first_name} #{user.last_name}"
15 | else
16 | "Sign Up: #{user.first_name} #{user.last_name}"
17 | end
18 | post_message(text)
19 | end
20 |
21 | defp post_message(text) do
22 | Chat.post_message("#general", text)
23 | Logger.info(text)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171105204155_create_events.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateEvents do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:events) do
6 | add :name, :string
7 | add :distance, :float
8 | add :date, :date
9 | add :warmup, :boolean, default: false, null: false
10 | add :cooldown, :boolean, default: false, null: false
11 | add :pace_id, references(:paces, on_delete: :delete_all), null: false
12 | add :user_id, references(:users, on_delete: :delete_all), null: false
13 |
14 | timestamps()
15 | end
16 |
17 | create index(:events, [:pace_id])
18 | create index(:events, [:user_id])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/assets/css/core/utilities/_opacity.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Opacity
3 | // modify the transparency of an element with this quick modifier classes
4 | //
5 |
6 | .opacity-1 {
7 | opacity: .1 !important;
8 | }
9 | .opacity-2 {
10 | opacity: .2 !important;
11 | }
12 | .opacity-3 {
13 | opacity: .3 !important;
14 | }
15 | .opacity-4 {
16 | opacity: .4 !important;
17 | }
18 | .opacity-5 {
19 | opacity: .5 !important;
20 | }
21 | .opacity-6 {
22 | opacity: .6 !important;
23 | }
24 | .opacity-7 {
25 | opacity: .7 !important;
26 | }
27 | .opacity-8 {
28 | opacity: .8 !important;
29 | }
30 | .opacity-8 {
31 | opacity: .9 !important;
32 | }
33 | .opacity-10 {
34 | opacity: 1 !important;
35 | }
36 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/forgot_password/form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @conn, @action, [class: "ui form"], fn f -> %>
2 |
13 |
14 | <%= submit gettext("Reset Password"), class: "btn btn-primary my-4" %>
15 |
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/lib/squeeze/strava/client.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Strava.Client do
2 | @moduledoc """
3 | Loads data from strava and updates the activity
4 | """
5 |
6 | alias Squeeze.Accounts
7 | alias Squeeze.Accounts.{Credential, User}
8 |
9 | def new(%User{} = user) do
10 | case Accounts.fetch_credential_by_provider(user, "strava") do
11 | {:ok, credential} -> new(credential)
12 | _ -> nil
13 | end
14 | end
15 |
16 | def new(%Credential{provider: "strava"} = credential) do
17 | Strava.Client.new(credential.access_token,
18 | refresh_token: credential.refresh_token,
19 | token_refreshed: &Accounts.update_credential(credential, Map.from_struct(&1.token))
20 | )
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/assets/css/core/grid/_grid.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Grid
3 | //
4 |
5 |
6 | // Example row
7 |
8 | .row-example {
9 | > .col,
10 | > [class^="col-"] {
11 | span {
12 | display: block;
13 | padding: .75rem;
14 | color: rgb(57, 63, 73);
15 | background-color: rgb(255, 255, 255);
16 | box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 4px 16px;
17 | font-size: $font-size-sm;
18 | border-radius: .25rem;
19 | margin: 1rem 0;
20 | }
21 | }
22 | }
23 |
24 | .no-gutters {
25 | > .col,
26 | > [class^="col-"] {
27 | span {
28 | border-radius: 0;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/squeeze/release.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Release do
2 | @moduledoc """
3 | Used for executing DB release tasks when run in production without Mix
4 | installed.
5 | """
6 | @app :squeeze
7 |
8 | def migrate do
9 | load_app()
10 |
11 | for repo <- repos() do
12 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
13 | end
14 | end
15 |
16 | def rollback(repo, version) do
17 | load_app()
18 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
19 | end
20 |
21 | defp repos do
22 | Application.fetch_env!(@app, :ecto_repos)
23 | end
24 |
25 | defp load_app do
26 | Application.load(@app)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20200708041417_create_result_summaries.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateResultSummaries do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:race_result_summaries) do
6 | add :distance, :float
7 | add :distance_name, :string
8 | add :start_date, :date
9 | add :finisher_count, :integer
10 | add :male_winner_time, :integer
11 | add :female_winner_time, :integer
12 | add :male_avg_time, :integer
13 | add :female_avg_time, :integer
14 |
15 | add :race_id, references(:races, on_delete: :delete_all), null: false
16 |
17 | timestamps()
18 | end
19 |
20 | create index(:race_result_summaries, [:race_id])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/squeeze/challenges/score.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Challenges.Score do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 |
7 | alias Squeeze.Accounts.User
8 | alias Squeeze.Challenges.Challenge
9 |
10 | schema "scores" do
11 | field :amount, :float
12 |
13 | # Used for ranking only and not visible to the user
14 | field :score, :float
15 |
16 | belongs_to :user, User
17 | belongs_to :challenge, Challenge
18 |
19 | timestamps()
20 | end
21 |
22 | @doc false
23 | def changeset(score, attrs \\ %{}) do
24 | score
25 | |> cast(attrs, [:amount])
26 | |> unique_constraint(:user, name: :scores_user_id_challenge_id_index, message: "already joined")
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/squeeze_web/controllers/challenge_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ChallengeController do
2 | use SqueezeWeb, :controller
3 | @moduledoc false
4 |
5 | alias Squeeze.Challenges
6 | alias Squeeze.Notifications
7 |
8 | def action(conn, _) do
9 | args = [conn, conn.params, conn.assigns.current_user]
10 | apply(__MODULE__, action_name(conn), args)
11 | end
12 |
13 | def join(conn, %{"id" => slug}, user) do
14 | challenge = Challenges.get_challenge_by_slug!(slug)
15 |
16 | with {:ok, _} <- Challenges.add_user_to_challenge(user, challenge) do
17 | Notifications.notify_user_joined(challenge, user)
18 | redirect(conn, to: Routes.challenge_path(conn, :show, challenge.slug))
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/squeeze_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ErrorViewTest do
2 | use SqueezeWeb.ConnCase, async: true
3 |
4 | @moduletag :error_view_case
5 |
6 | # Bring render/3 and render_to_string/3 for testing custom views
7 | import Phoenix.View
8 |
9 | test "renders 404.html" do
10 | assert render_to_string(SqueezeWeb.ErrorView, "404.html", []) ==
11 | "Page not found"
12 | end
13 |
14 | test "render 500.html" do
15 | assert render_to_string(SqueezeWeb.ErrorView, "500.html", []) ==
16 | "Internal server error"
17 | end
18 |
19 | test "render any other" do
20 | assert render_to_string(SqueezeWeb.ErrorView, "505.html", []) ==
21 | "Internal server error"
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/assets/css/custom/_functions.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Custom functions
3 | //
4 |
5 |
6 | // Retrieve color Sass maps
7 |
8 | @function section-color($key: "primary") {
9 | @return map-get($section-colors, $key);
10 | }
11 |
12 |
13 | // Lines colors
14 |
15 | @function shapes-primary-color($key: "step-1-gradient-bg") {
16 | @return map-get($shapes-primary-colors, $key);
17 | }
18 |
19 | @function shapes-default-color($key: "step-1-gradient-bg") {
20 | @return map-get($shapes-default-colors, $key);
21 | }
22 |
23 | @function lines-light-color($key: "step-1-gradient-bg") {
24 | @return map-get($shapes-light-colors, $key);
25 | }
26 |
27 | @function shapes-dark-color($key: "step-1-gradient-bg") {
28 | @return map-get($shapes-dark-colors, $key);
29 | }
30 |
--------------------------------------------------------------------------------
/lib/squeeze/fitbit/history_loader.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Fitbit.HistoryLoader do
2 | @moduledoc false
3 |
4 | alias Squeeze.Accounts
5 | alias Squeeze.Accounts.{Credential}
6 | alias Squeeze.Fitbit.{ActivityLoader, Client}
7 |
8 | def load_recent(%Credential{} = credential) do
9 | case fetch_activities(credential) do
10 | {:ok, response} ->
11 | response.body["activities"]
12 | |> Enum.each(&(ActivityLoader.update_or_create_activity(credential, &1)))
13 | Accounts.update_credential(credential, %{sync_at: Timex.now})
14 | _ -> {:error}
15 | end
16 | end
17 |
18 | defp fetch_activities(credential) do
19 | credential
20 | |> Client.new()
21 | |> Client.get_activities()
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/challenge_live.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.ChallengeLive do
2 | use SqueezeWeb, :live_view
3 | @moduledoc false
4 |
5 | alias Squeeze.Challenges
6 | alias Squeeze.Guardian
7 | alias Squeeze.TimeHelper
8 |
9 | @impl true
10 | def mount(_params, %{"guardian_default_token" => token}, socket) do
11 | {:ok, user, _claims} = Guardian.resource_from_token(token)
12 | date = TimeHelper.today(user) |> Timex.shift(days: -3)
13 | socket = socket
14 | |> assign(page_title: "Challenges")
15 | |> assign(current_user: user)
16 | |> assign(challenges: Challenges.list_challenges(user, ends_after: date))
17 | |> assign(podium_finishes: Challenges.podium_finishes(user))
18 |
19 | {:ok, socket}
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/squeeze/challenges/challenge_activity.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Challenges.ChallengeActivity do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 |
7 | alias Squeeze.Challenges.Challenge
8 | alias Squeeze.Dashboard.Activity
9 |
10 | @required_fields ~w(amount)a
11 | @optional_fields ~w()a
12 |
13 | schema "challenge_activities" do
14 | field :amount, :float
15 |
16 | belongs_to :activity, Activity
17 | belongs_to :challenge, Challenge
18 |
19 | timestamps()
20 | end
21 |
22 | @doc false
23 | def changeset(challenge_activity, attrs \\ %{}) do
24 | challenge_activity
25 | |> cast(attrs, @required_fields ++ @optional_fields)
26 | |> validate_required(@required_fields)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/assets/js/components/sign-up-form.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 | import { u } from 'umbrellajs';
3 |
4 | function init() {
5 | const $form = u('#sign-up-form');
6 |
7 | $form.handle('submit', (e) => {
8 | const action = $form.attr('action');
9 | const method = $form.attr('method');
10 | const options = {method: method, body: new FormData($form.first())};
11 |
12 | fetch(action, options)
13 | .then(resp => Promise.all([resp, resp.text()]))
14 | .then(([resp, body]) => {
15 | if (resp.status === 200 || resp.status === 201) {
16 | $('#auth-modal').modal('hide');
17 | } else {
18 | const html = u(body).filter('form').html();
19 | $form.html(html);
20 | }
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/lib/squeeze/billing/invoice.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Billing.Invoice do
2 | @moduledoc false
3 |
4 | use Ecto.Schema
5 | import Ecto.Changeset
6 | alias Squeeze.Accounts.User
7 |
8 | @required_fields ~w(name amount_due due_date provider_id status)a
9 | @optional_fields ~w()
10 |
11 | schema "billing_invoices" do
12 | field :name, :string
13 | field :amount_due, :integer
14 | field :provider_id, :string
15 | field :status, :string
16 | field :due_date, :utc_datetime
17 |
18 | belongs_to :user, User
19 |
20 | timestamps()
21 | end
22 |
23 | @doc false
24 | def changeset(plan, attrs) do
25 | plan
26 | |> cast(attrs, @required_fields ++ @optional_fields)
27 | |> validate_required(@required_fields)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20190715014941_create_training_events.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Repo.Migrations.CreateTrainingEvents do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:training_events) do
6 | add :name, :string
7 | add :distance, :float
8 | add :duration, :integer
9 |
10 | add :warmup, :boolean, default: false, null: false
11 | add :cooldown, :boolean, default: false, null: false
12 |
13 | add :plan_position, :integer, default: 0
14 | add :day_position, :integer, default: 0
15 |
16 | add :training_plan_id, references(:training_plans, on_delete: :delete_all), null: false
17 |
18 | timestamps()
19 | end
20 |
21 | create index(:training_events, [:training_plan_id])
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/assets/css/core/collapse/_accordion.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Accordion
3 | //
4 |
5 |
6 | .accordion {
7 | .card-header {
8 | position: relative;
9 | cursor: pointer;
10 |
11 | &:after {
12 | content: "\ea0f";
13 | position: absolute;
14 | right: 1.5rem;
15 | top: 50%;
16 | transform: translateY(-50%);
17 | font: normal normal normal 14px/1 NucleoIcons;
18 | line-height: 0;
19 | @include transition($transition-cubic-bezier);
20 | }
21 | }
22 |
23 | .card-header[aria-expanded="false"] {
24 | &:after {
25 | content: "\ea0f";
26 | }
27 | }
28 |
29 | .card-header[aria-expanded="true"] {
30 | &:after {
31 | transform: rotate(180deg);
32 | }
33 |
34 | .heading {
35 | color: theme-color("primary");
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/race/_schema.html.eex:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/assets/css/core/icons/_icon-shape.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Icon shape
3 | //
4 |
5 |
6 | .icon-shape {
7 | padding: 12px;
8 | text-align: center;
9 | display: inline-flex;
10 | align-items: center;
11 | justify-content: center;
12 | border-radius: 50%;
13 |
14 |
15 | i, svg {
16 | font-size: 1.25rem;
17 | }
18 |
19 | &.icon-lg {
20 | i, svg {
21 | font-size: 1.625rem;
22 | }
23 | }
24 |
25 | &.icon-sm {
26 | i, svg {
27 | font-size: .875rem;
28 | }
29 | }
30 |
31 | &.icon-xs {
32 | i, svg {
33 | font-size: .6rem;
34 | }
35 | }
36 |
37 | svg {
38 | width: 30px;
39 | height: 30px;
40 | }
41 |
42 | }
43 |
44 | @each $color, $value in $theme-colors {
45 | .icon-shape-#{$color} {
46 | @include icon-shape-variant(theme-color($color));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/squeeze/social/follow.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Social.Follow do
2 | @moduledoc """
3 | This module is a schema for follows
4 | """
5 |
6 | use Ecto.Schema
7 | import Ecto.Changeset
8 |
9 | alias Squeeze.Accounts.{User}
10 |
11 | @required_fields ~w()a
12 | @optional_fields ~w(pending)a
13 |
14 | schema "follows" do
15 | field :pending, :boolean
16 |
17 | belongs_to :follower, User
18 | belongs_to :followee, User
19 |
20 | timestamps()
21 | end
22 |
23 | @doc false
24 | def changeset(follow, attrs \\ %{}) do
25 | follow
26 | |> cast(attrs, @required_fields ++ @optional_fields)
27 | |> validate_required(@required_fields)
28 | |> unique_constraint(:unique_follow, name: :follows_follower_id_followee_id_index)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/factories/detailed_activity_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.DetailedActivityFactory do
2 | @moduledoc false
3 |
4 | alias Strava.DetailedActivity
5 |
6 | defmacro __using__(_opts) do
7 | quote do
8 | def detailed_activity_factory do
9 | timezone = "America/New_York"
10 |
11 | %DetailedActivity{
12 | id: sequence(:external_id, &(&1)),
13 | name: Enum.random(["Morning Run", "Afternoon Run", "Evening Run"]),
14 | distance: 5000.0,
15 | moving_time: 1_200, # 20 minutes
16 | start_date: Timex.now(),
17 | start_date_local: Timex.to_datetime(Timex.now(), timezone),
18 | map: %{summary_polyline: "ABCDEF"},
19 | type: "Run"
20 | }
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/assets/css/core/mixins/_popover.scss:
--------------------------------------------------------------------------------
1 | @mixin popover-variant($background) {
2 |
3 | background-color: $background;
4 |
5 | .popover-header {
6 | background-color: $background;
7 | color: color-yiq($background);
8 | }
9 |
10 | .popover-body {
11 | color: color-yiq($background);
12 | }
13 | .popover-header{
14 | border-color: rgba(color-yiq($background), .2);
15 | }
16 | &.bs-popover-top {
17 | .arrow::after {
18 | border-top-color: $background;
19 | }
20 | }
21 | &.bs-popover-right {
22 | .arrow::after {
23 | border-right-color: $background;
24 | }
25 | }
26 | &.bs-popover-bottom {
27 | .arrow::after {
28 | border-bottom-color: $background;
29 | }
30 | }
31 | &.bs-popover-left {
32 | .arrow::after {
33 | border-left-color: $background;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/lib/squeeze_web/live/dashboard/mini_calendar_component.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Dashboard.MiniCalendarComponent do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | alias Squeeze.TimeHelper
6 |
7 | def data(%{activity_map: activity_map} = assigns) do
8 | dates(assigns)
9 | |> Enum.map(fn(date) ->
10 | %{
11 | date: date,
12 | activities: Map.get(activity_map, date, [])
13 | }
14 | end)
15 | end
16 |
17 | def dates(assigns) do
18 | today = today(assigns)
19 | end_date = Timex.end_of_week(today)
20 | start_date = today |> Timex.shift(weeks: -4) |> Timex.beginning_of_week()
21 | Date.range(start_date, end_date)
22 | end
23 |
24 | def today(%{current_user: user}) do
25 | TimeHelper.today(user)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/squeeze/garmin/middleware/oauth.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Garmin.Middleware.OAuth do
2 | @moduledoc false
3 |
4 | @behaviour Tesla.Middleware
5 |
6 | @config Application.compile_env(:squeeze, Squeeze.Garmin)
7 |
8 | alias Squeeze.OAuth1
9 |
10 | def call(env, next, opts) do
11 | creds = oauth_credentials(opts)
12 | method = env.method |> Atom.to_string() |> String.upcase()
13 | params = OAuth1.sign(method, env.url, env.query, creds)
14 | {header, _req_params} = OAuth1.header(params)
15 | env
16 | |> Tesla.put_headers([header])
17 | |> Tesla.run(next)
18 | end
19 |
20 | defp oauth_credentials(opts) do
21 | @config
22 | |> Keyword.take([:consumer_key, :consumer_secret])
23 | |> Keyword.merge(opts)
24 | |> OAuth1.credentials()
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/modal/past-due.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Your trial has ended
7 |
Please head over to billing and update your payment information.
8 |
9 |
10 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/squeeze/mailing_list/mailing_list_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.MailingListTest do
2 | use Squeeze.DataCase
3 |
4 | alias Squeeze.MailingList
5 |
6 | describe "subscriptions" do
7 | alias Squeeze.MailingList.Subscription
8 |
9 | test "create_subscription/1 with valid data creates a subscription" do
10 | assert {:ok, %Subscription{} = subscription} =
11 | MailingList.create_subscription(%{email: "test@email.com", type: "some type"})
12 | assert subscription.email == "test@email.com"
13 | assert subscription.type == "some type"
14 | end
15 |
16 | test "create_subscription/1 with invalid data returns error changeset" do
17 | assert {:error, %Ecto.Changeset{}} =
18 | MailingList.create_subscription(%{email: ""})
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/live_auth.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.LiveAuth do
2 | @moduledoc """
3 | Assign current_user from the session token.
4 | """
5 | import Phoenix.LiveView
6 |
7 | alias Squeeze.Guardian
8 |
9 | @token_key "guardian_default_token"
10 |
11 | def on_mount(:default, _params, session, socket) do
12 | socket = assign_new(socket, :current_user, fn -> get_current_user(session) end)
13 |
14 | if socket.assigns.current_user do
15 | {:cont, socket}
16 | else
17 | {:halt, redirect(socket, to: "/logout")}
18 | end
19 | end
20 |
21 | defp get_current_user(%{@token_key => token}) do
22 | case Guardian.resource_from_token(token) do
23 | {:ok, user, _claims} -> user
24 | _ -> nil
25 | end
26 | end
27 | defp get_current_user(_), do: nil
28 | end
29 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/reset_password/form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, [class: "ui form"], fn f -> %>
2 |
13 |
14 |
15 | <%= submit gettext("Reset Password"), class: "btn btn-spinner btn-primary my-4" %>
16 |
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/lib/squeeze_web/live/race_live/upcoming_races_card.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.RaceLive.UpcomingRacesCard do
2 | use SqueezeWeb, :live_component
3 | @moduledoc false
4 |
5 | alias Squeeze.Distances
6 |
7 | @colors ~w(
8 | blue
9 | indigo
10 | purple
11 | red
12 | orange
13 | green
14 | teal
15 | cyan
16 | )
17 |
18 | def race_date(%{start_date: date}) do
19 | date
20 | |> Timex.format!("%B #{Ordinal.ordinalize(date.day)}, %Y", :strftime)
21 | end
22 |
23 | def bg_color(model) do
24 | idx = rem(model.id, length(@colors))
25 | color = Enum.at(@colors, idx)
26 | "bg-gradient-#{color}"
27 | end
28 |
29 | def distance_name(distance, current_user) do
30 | Distances.distance_name(distance, imperial: current_user.user_prefs.imperial)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/squeeze_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import SqueezeWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :squeeze
24 | end
25 |
--------------------------------------------------------------------------------
/test/factories/payment_method_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.PaymentMethodFactory do
2 | @moduledoc false
3 |
4 | alias Faker.{Address, Lorem, Person}
5 | alias Squeeze.Billing.PaymentMethod
6 |
7 | defmacro __using__(_opts) do
8 | quote do
9 | def payment_method_factory do
10 | company = Enum.random(["Visa", "Mastercard", "American Express"])
11 | owner = Person.name()
12 |
13 | %PaymentMethod{
14 | owner_name: owner,
15 | address_zip: Address.zip(),
16 |
17 | exp_month: Enum.random(1..12),
18 | exp_year: Timex.today.year + Enum.random(1..6),
19 | last4: "#{Enum.random(1000..9999)}",
20 | stripe_id: "card_#{Lorem.characters(15)}",
21 | user: build(:user)
22 | }
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/squeeze/email.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Email do
2 | use Bamboo.Phoenix, view: SqueezeWeb.EmailView
3 |
4 | @moduledoc false
5 |
6 | alias Squeeze.CompanyHelper
7 |
8 | def welcome_email(user) do
9 | base_email()
10 | |> to(user.email)
11 | |> subject("Welcome to OpenPace!")
12 | |> assign(:user, user)
13 | |> render(:welcome)
14 | end
15 |
16 | def reset_password_email(user, link) do
17 | base_email()
18 | |> to(user.email)
19 | |> subject("Your Reset Password Link")
20 | |> assign(:user, user)
21 | |> assign(:link, link)
22 | |> render(:reset_password)
23 | end
24 |
25 | defp base_email do
26 | new_email()
27 | |> from(CompanyHelper.team_email())
28 | |> bcc(CompanyHelper.team_email())
29 | |> put_layout({SqueezeWeb.LayoutView, :email})
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/squeeze_web/views/api/challenge_activity_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SqueezeWeb.Api.ChallengeActivityView do
2 | use SqueezeWeb, :view
3 | @moduledoc false
4 |
5 | def render("index.json", %{challenge_activities: challenge_activities}) do
6 | %{challenge_activities: render_many(challenge_activities, SqueezeWeb.Api.ChallengeActivityView, "challenge_activity.json")}
7 | end
8 |
9 | def render("challenge_activity.json", %{challenge_activity: challenge_activity}) do
10 | activity = challenge_activity.activity
11 |
12 | %{
13 | id: challenge_activity.id,
14 | amount: challenge_activity.amount,
15 | activity: render_one(activity, SqueezeWeb.Api.ActivityView, "activity.json", as: :activity),
16 | user: render_one(activity.user, SqueezeWeb.Api.UserView, "user.json", as: :user)
17 | }
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/assets/css/core/vendors/_datatables.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Datatables
3 | //
4 |
5 | .dataTables_wrapper {
6 | font-size: $font-size-sm;
7 | }
8 |
9 | table.dataTable {
10 | margin-bottom: $card-spacer-y !important;
11 | border-bottom: 1px solid $table-border-color;
12 |
13 | tbody {
14 | > tr.selected {
15 | background-color: theme-color("primary");
16 | }
17 | }
18 | }
19 |
20 | .dataTables_length,
21 | .dataTables_info,
22 | .dt-buttons {
23 | padding-left: $card-spacer-x;
24 | }
25 |
26 | .dataTables_length {
27 | .form-control {
28 | margin: 0 .375rem;
29 | }
30 | }
31 |
32 | .dataTables_filter {
33 | padding-right: $card-spacer-x;
34 | display: inline-block;
35 | float: right;
36 | }
37 |
38 | .dataTables_paginate {
39 | padding-right: $card-spacer-x;
40 | }
41 |
--------------------------------------------------------------------------------
/lib/squeeze/stringable.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Stringable do
2 | @moduledoc """
3 | This ecto type allows for both integers and strings. This is used when we want
4 | to store segment_id (an external identifier) as a string and not an integer.
5 | """
6 | @behaviour Ecto.Type
7 |
8 | def cast(s) when is_binary(s), do: {:ok, s}
9 |
10 | def cast(val) do
11 | if String.Chars.impl_for(val) do
12 | {:ok, to_string(val)}
13 | else
14 | {:error, "cannot convert to string"}
15 | end
16 | end
17 |
18 | def dump(str) do
19 | Ecto.Type.dump(:string, str)
20 | end
21 |
22 | def load(str) do
23 | Ecto.Type.load(:string, str)
24 | end
25 |
26 | def type do
27 | Ecto.Type.type(:string)
28 | end
29 |
30 | def embed_as(_), do: :self
31 |
32 | def equal?(left, right), do: left == right
33 | end
34 |
--------------------------------------------------------------------------------
/lib/squeeze/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Utils do
2 | @moduledoc """
3 | Utility functions
4 | """
5 |
6 | def key_to_atom(map) do
7 | Enum.reduce(map, %{}, fn
8 | # String.to_existing_atom saves us from overloading the VM by
9 | # creating too many atoms. It'll always succeed because all the fields
10 | # in the database already exist as atoms at runtime.
11 | {key, value}, acc when is_atom(key) -> Map.put(acc, key, value)
12 | {key, value}, acc when is_binary(key) -> Map.put(acc, String.to_existing_atom(key), value)
13 | end)
14 | end
15 |
16 | def random_float(a, b), do: a + (b - a) * :rand.uniform()
17 |
18 | def random_int(a, b), do: a + :rand.uniform(b - a)
19 |
20 | def cast_float(nil), do: nil
21 | def cast_float(x) when is_integer(x), do: x * 1.0
22 | def cast_float(x), do: x
23 | end
24 |
--------------------------------------------------------------------------------
/test/squeeze/challenges/score_updater_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Squeeze.Challenges.ScoreUpdaterTest do
2 | use Squeeze.DataCase
3 |
4 | # import Squeeze.Factory
5 |
6 | # alias Ecto.Changeset
7 | # alias Squeeze.Challenges
8 | # alias Squeeze.Challenges.{Challenge, Score}
9 |
10 | # describe "#total_amount/2" do
11 | # test "with segment challenge" do
12 | # end
13 |
14 | # test "with multiple activities" do
15 | # end
16 | # end
17 |
18 | # describe "#amount/2" do
19 | # test "with a distance challenge" do
20 | # insert(:challenge, challenge_type: :distance) |> with_scores(1)
21 | # end
22 |
23 | # test "with a time challenge" do
24 | # end
25 |
26 | # test "with an altitude challenge" do
27 | # end
28 |
29 | # test "with a segment challenge" do
30 | # end
31 | # end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/squeeze_web/templates/shared/flash.html.eex:
--------------------------------------------------------------------------------
1 | <% info_msg = get_flash(@conn, :info) %>
2 | <% error_msg = get_flash(@conn, :error) %>
3 |
4 | <%= if info_msg do %>
5 |
6 | <%= info_msg %>
7 |
10 |
11 | <% end %>
12 |
13 | <%= if error_msg do %>
14 |
15 | <%= error_msg %>
16 |
19 |
20 | <% end %>
21 |
--------------------------------------------------------------------------------
/assets/js/components/distance-select.js:
--------------------------------------------------------------------------------
1 | import SlimSelect from 'slim-select';
2 | import { parseDistance } from '../utils';
3 |
4 | function init() {
5 | $('.distance-select').each((_, el) => {
6 | new SlimSelect({
7 | select: el,
8 | // Optional - In the event you want to alter/validate it as a return value
9 | addable: function (value) {
10 | const distance = parseDistance(value);
11 |
12 | // return false or null if you do not want to allow value to be submitted
13 | if (distance === null) { return false; }
14 |
15 | // Optional - Return a valid data object. See methods/setData for list of valid options
16 | return {
17 | text: value,
18 | value: distance,
19 | };
20 | }
21 | });
22 | });
23 | }
24 |
25 | window.addEventListener("phx:page-loading-stop", init);
26 |
--------------------------------------------------------------------------------