├── .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 | {full_name(@user)} 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 |
3 | <%= input f, :email, placeholder: gettext("Email"), class: "form-control" %> 4 | 5 |
6 | <%= submit gettext("Request Access"), class: "btn btn-primary" %> 7 |
8 |
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 | <%= user.first_name %> 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 | 4 | 5 | 6 | 7 | 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 | {"#{@segment.name} 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 | 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 | 8 | 11 | 14 | 15 | 16 | 17 | 18 | <%= for event <- @race.events do %> 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
6 | Event 7 | 9 | Distance 10 | 12 | Start Time 13 |
<%= event.name %>
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 |
    4 |
    5 | 6 |
    7 |
    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 |
    3 |
    4 |
    5 | 6 | 7 | 8 |
    9 | <%= text_input f, :email, placeholder: gettext("Email"), class: "form-control" %> 10 | <%= error_tag f, :email %> 11 |
    12 |
    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 | 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 |
    3 |
    4 |
    5 | 6 | 7 | 8 |
    9 | <%= input f, :encrypted_password, type: "password", placeholder: gettext("Password"), value: "", class: "form-control form-control-alternative" %> 10 | <%= error_tag f, :encrypted_password %> 11 |
    12 |
    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 | 11 | <% end %> 12 | 13 | <%= if error_msg do %> 14 | 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 | --------------------------------------------------------------------------------