From 4b0420bdd911aac75d2ffdfb4ba3d66f8e938be7 Mon Sep 17 00:00:00 2001 From: Gerald <44438849+GMSoudersJr@users.noreply.github.com> Date: Sun, 7 Dec 2025 10:57:44 +0900 Subject: [PATCH 1/2] 111 privacy policy (#112) * feat(privacy): add privacy policy Add privacy policy page to the website. Still needs contact information. * feat(privacy): add contact detail * fix(privacy): remove unescaped characters --- src/app/(privacy)/privacy/default.tsx | 3 + src/app/(privacy)/privacy/layout.module.css | 6 + src/app/(privacy)/privacy/layout.tsx | 9 + src/app/(privacy)/privacy/page.module.css | 0 src/app/(privacy)/privacy/page.tsx | 8 + src/components/landing/Footer.tsx | 5 +- .../privacy/PrivacyPolicy.module.css | 26 ++ src/components/privacy/PrivacyPolicy.tsx | 269 ++++++++++++++++++ 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 src/app/(privacy)/privacy/default.tsx create mode 100644 src/app/(privacy)/privacy/layout.module.css create mode 100644 src/app/(privacy)/privacy/layout.tsx create mode 100644 src/app/(privacy)/privacy/page.module.css create mode 100644 src/app/(privacy)/privacy/page.tsx create mode 100644 src/components/privacy/PrivacyPolicy.module.css create mode 100644 src/components/privacy/PrivacyPolicy.tsx diff --git a/src/app/(privacy)/privacy/default.tsx b/src/app/(privacy)/privacy/default.tsx new file mode 100644 index 0000000..6ddf1b7 --- /dev/null +++ b/src/app/(privacy)/privacy/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/src/app/(privacy)/privacy/layout.module.css b/src/app/(privacy)/privacy/layout.module.css new file mode 100644 index 0000000..0ea8bd5 --- /dev/null +++ b/src/app/(privacy)/privacy/layout.module.css @@ -0,0 +1,6 @@ +.main { + padding: 0.5rem; + display: grid; + grid-template-columns: 100%; + justify-items: center; +} diff --git a/src/app/(privacy)/privacy/layout.tsx b/src/app/(privacy)/privacy/layout.tsx new file mode 100644 index 0000000..95c81db --- /dev/null +++ b/src/app/(privacy)/privacy/layout.tsx @@ -0,0 +1,9 @@ +import styles from "./layout.module.css"; + +export default function PrivacyPageLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/src/app/(privacy)/privacy/page.module.css b/src/app/(privacy)/privacy/page.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(privacy)/privacy/page.tsx b/src/app/(privacy)/privacy/page.tsx new file mode 100644 index 0000000..6fc8d63 --- /dev/null +++ b/src/app/(privacy)/privacy/page.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import PrivacyPolicy from "@/components/privacy/PrivacyPolicy"; + +function PrivacyPage() { + return ; +} + +export default PrivacyPage; diff --git a/src/components/landing/Footer.tsx b/src/components/landing/Footer.tsx index 6c199ab..295f23e 100644 --- a/src/components/landing/Footer.tsx +++ b/src/components/landing/Footer.tsx @@ -20,8 +20,11 @@ const Footer = () => { > PDF SOURCE MATERIAL + + PRIVACY POLICY + diff --git a/src/components/privacy/PrivacyPolicy.module.css b/src/components/privacy/PrivacyPolicy.module.css new file mode 100644 index 0000000..ab4df4d --- /dev/null +++ b/src/components/privacy/PrivacyPolicy.module.css @@ -0,0 +1,26 @@ +.privacyPolicy { + max-width: 37.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.orderedList, +.unorderedList, +.listitem { + list-style-type: revert; + list-style-position: revert; + margin: revert; + padding: revert; +} + +.listitem > section > p, +.listitem > * { + line-height: 1.5; +} + +.orderedList { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/src/components/privacy/PrivacyPolicy.tsx b/src/components/privacy/PrivacyPolicy.tsx new file mode 100644 index 0000000..c3a348e --- /dev/null +++ b/src/components/privacy/PrivacyPolicy.tsx @@ -0,0 +1,269 @@ +import Link from "next/link"; +import React from "react"; +import styles from "./PrivacyPolicy.module.css"; + +const lastUpdatedDate = "December 8th, 2025"; +const mainGooglePrivacyPolicy = "https://policies.google.com/privacy"; +const otherGooglePrivacyPolicy = + "https://policies.google.com/technologies/partner-sites"; +const emailAddress = "repyourself.privacy@gmail.com"; + +export default function PrivacyPolicy() { + return ( +
+

Rep Yourself - Privacy Policy

+
+

+ last updated: +

+
+
    +
  1. +
    +
    Introduction
    +

    + Welcome to Rep Yourself! Our commitment is to help you achieve + your fitness goals. To improve your experience, our app uses + anonymous analytics to understand how its features are used. This + Privacy Policy explains what data is collected, why it is + collected, and how you can control it. Your privacy and trust are + our top priorities. +

    +
    +
  2. +
  3. +
    +
    What Information We Collect
    +

    + Rep Yourself collects anonymous, non-personal data to help us + improve the app. We do not collect personal information like your + name or email, and we do not track or store the specific number of + reps or sets you perform in your workouts. The anonymous + information we collect falls into two categories: +

    +
      +
    • + App Usage Information: + We log key events to understand how you interact with the app. + This includes actions like starting a new program, choosing a + grip type on Day 3, starting or skipping a workout, and viewing + your workout history. +
    • +
    • + Diagnostic Information: + To keep the app running smoothly, we collect data about + technical events. This includes tracking when a data migration + from the old app fails and the general reason for the failure, + such as an invalid file format. We also note whether permissions + for features like notifications are granted or denied. +
    • +
    +
    +
  4. +
  5. +
    +
    How We Use This Information
    +

    + We use this anonymous and aggregated data for one purpose: to make + Rep Yourself a better app. +

    +
      +
    • + To Improve Features: + By seeing which features are used most and where users might be + having trouble, we can focus our efforts on making the app more + intuitive and effective. For example, if we notice many users + skip a particular workout, we can investigate if it needs to be + adjusted. +
    • +
    • + To Fix Bugs: + Diagnostic information helps us quickly identify, diagnose, and + fix bugs and crashes. Seeing patterns in migration errors, for + instance, allows us to make the process more reliable for + everyone. +
    • +
    • + To Make Better Decisions: + Understanding how the app is used helps us make informed + decisions about what new features to build and which to improve, + ensuring we spend our time on what matters most to our users. +
    • +
    +
    +
  6. +
  7. +
    +
    Third-Party Services & Data Sharing
    +

    + To provide our services and to understand how our app is used, Rep + Yourself relies on third-party services that may collect + information from your device. We do not share your data with any + third parties other than those listed below, which are essential + for the app`&apos`s functionality and improvement. +

    +
      +
    • + Firebase Analytics (a Google Service) +

      + We use Firebase Analytics to collect the anonymous usage and + diagnostic data described in this policy. This service helps + us understand user behavior, identify popular features, and + diagnose bugs. The data sent to Firebase is aggregated and + does not personally identify you. +

      +

      + As Firebase is a Google product, its use is governed by + Google`&apos`s privacy practices. We strongly encourage you to + review their policies to understand how they handle data. +

      +
        +
      • + Google Privacy Policy: + + {mainGooglePrivacyPolicy} + +
      • +
      • + + How Google uses information from sites or apps that use + our services:{" "} + + + {otherGooglePrivacyPolicy} + +
      • +
      +
    • +
    +
    +
  8. +
  9. +
    +
    Your Choices and How to Opt-Out
    +

    + You are in full control of your data. You can choose to disable + the collection of anonymous usage data at any time. To do so, + please navigate to the Settings screen within the Rep Yourself app + and turn off the `"`Share anonymous usage data`"` toggle. + This will stop any future data from being sent. +

    +
    +
  10. +
  11. +
    +
    Your Rights Under GDPR
    +

    + If you are a resident of the European Economic Area (EEA), you + have certain data protection rights under the General Data + Protection Regulation (GDPR). Rep Yourself is committed to + upholding these rights. While our app collects only anonymous and + non-personal usage data, we recognize your rights to: +

    +
      +
    • + + The right to access, update, or delete the information we have + on you.{" "} + + Since the data we collect is anonymous, we cannot identify and + retrieve data for a specific person. However, you can + effectively delete your data by resetting the app`&apos`s + advertising ID or by using the `"`Reset Program`"` + function, which deletes all locally stored workout history. +
    • +
    • + The right of rectification. If you believe any + data is inaccurate, you have the right to have it rectified. +
    • +
    • + The right to object. You have the right to + object to our processing of your data. You can exercise this + right by disabling analytics collection in the app`&apos`s + Settings screen. +
    • +
    • + The right of portability. You have the right to + be provided with a copy of your data in a structured, + machine-readable format. Your workout history can be exported + from the app for this purpose. +
    • +
    • + The right to withdraw consent. You have the + right to withdraw your consent at any time where Rep Yourself + relied on your consent to process your information. You can do + this by toggling off analytics in the Settings screen. +
    • +
    +
    +
  12. +
  13. +
    +
    Your California Privacy Rights (CCPA/CPRA)
    +

    + If you are a California resident, you have specific rights under the + California Consumer Privacy Act (CCPA) and the California Privacy + Rights Act (CPRA). +

    +
      +
    • + Right to Know and Access: You have the right to + know what categories of information we collect and the purposes + for which we use it. This is outlined in the `"`Information We + Collect`"` and `"`How We Use This Information`"` + sections of this policy. +
    • +
    • + Right to Opt-Out of Sale or Sharing: Rep Yourself + does not sell or share your personal information with third + parties for cross-context behavioral advertising. As such, there + is no `"`sale`"` or `"`sharing`"` to opt out of. + We only share anonymous data with our analytics provider, Google, + for the sole purpose of improving our own app. +
    • +
    • + + Right to Limit Use of Sensitive Personal Information:{" "} + + We do not collect `"`Sensitive Personal Information`"` as + defined by California law. +
    • +
    • + Right to Deletion: You have the right to request + the deletion of your information. This can be accomplished by + using the `"`Reset Program`"` function in the app`&apos`s + settings, which will permanently delete all your stored workout + data. +
    • +
    • + Non-Discrimination: We will not discriminate + against you for exercising any of your CCPA/CPRA rights. +
    • +
    +
  14. +
  15. +
    +
    Changes to This Privacy Policy
    +

    + I may update our Privacy Policy from time to time. Thus, you are + advised to review this page periodically for any changes. I will + notify you of any changes by posting the new Privacy Policy on + this page. These changes are effective immediately after they are + posted on this page. +

    +
    +
  16. +
  17. +
    +
    Contact
    +

    + If you have any questions or suggestions about the Privacy Policy, + do not hesitate to contact me. +

    +

    {emailAddress}

    +
    +
  18. +
+
+ ); +} From a2798add9889d1befdc0a984d6fb1abe872521a0 Mon Sep 17 00:00:00 2001 From: Gerald <44438849+GMSoudersJr@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:06:32 +0900 Subject: [PATCH 2/2] 110 data migration (#114) * feat: implement download data button users can download their workout history to make it available for import to the native Android app in development * refactor: update name of backup file --- src/app/lib/utils.ts | 10 ++++ .../program/DownloadDataButton.module.css | 9 ++++ src/components/program/DownloadDataButton.tsx | 51 +++++++++++++++++++ .../program/PastWorkouts.module.css | 2 +- src/components/program/PastWorkouts.tsx | 2 + 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/components/program/DownloadDataButton.module.css create mode 100644 src/components/program/DownloadDataButton.tsx diff --git a/src/app/lib/utils.ts b/src/app/lib/utils.ts index 805e447..6c0c9fd 100644 --- a/src/app/lib/utils.ts +++ b/src/app/lib/utils.ts @@ -14,3 +14,13 @@ export async function getAllDailyInstructions(): Promise { export const isSingular = (count: number): boolean => { return count === 1; }; + +export const isSafari = () => { + const ua = navigator.userAgent; + return /^((?!chrome|android).)*safari/i.test(ua); +}; + +export const isChrome = () => { + const ua = navigator.userAgent; + return /chrome/i.test(ua) && !isSafari(); +}; diff --git a/src/components/program/DownloadDataButton.module.css b/src/components/program/DownloadDataButton.module.css new file mode 100644 index 0000000..56e27b6 --- /dev/null +++ b/src/components/program/DownloadDataButton.module.css @@ -0,0 +1,9 @@ +.button { + width: 100%; + background-color: #d1d1d1; + border-radius: 0.5rem; + border: none; + font-size: 1rem; + font-weight: 700; + grid-column: 1/-1; +} diff --git a/src/components/program/DownloadDataButton.tsx b/src/components/program/DownloadDataButton.tsx new file mode 100644 index 0000000..5b9b5cb --- /dev/null +++ b/src/components/program/DownloadDataButton.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { nunito } from "@/fonts"; +import styles from "./DownloadDataButton.module.css"; +import { getOverallProgess } from "@/indexedDBActions"; +import { isSafari } from "@/utils"; + +const DownloadDataButton = () => { + const [showButton, setShowButton] = useState(false); + + useEffect(() => { + if (!isSafari()) { + setShowButton(true); + } + }, []); + + async function handleDownload() { + const data = await getOverallProgess(); + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = "Pull-Up-App-Backup.json"; // Friendly filename + a.style.display = "none"; + + document.body.appendChild(a); + a.click(); + + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + if (!showButton) return; + + return ( + + ); +}; + +export default DownloadDataButton; diff --git a/src/components/program/PastWorkouts.module.css b/src/components/program/PastWorkouts.module.css index 582380b..48515c3 100644 --- a/src/components/program/PastWorkouts.module.css +++ b/src/components/program/PastWorkouts.module.css @@ -2,7 +2,7 @@ display: grid; height: 100%; grid-template-columns: 100%; - grid-template-rows: repeat(2, min-content); + grid-template-rows: repeat(3, min-content); justify-items: center; place-self: start; justify-self: center; diff --git a/src/components/program/PastWorkouts.tsx b/src/components/program/PastWorkouts.tsx index bca0d05..c5388b7 100644 --- a/src/components/program/PastWorkouts.tsx +++ b/src/components/program/PastWorkouts.tsx @@ -7,6 +7,7 @@ import { getWeeklyProgress } from "@/indexedDBActions"; import { CalendarArrowDownIcon, CalendarArrowUpIcon } from "lucide-react"; import { ReviewLink } from "./ReviewLink"; import { nunito } from "@/fonts"; +import DownloadDataButton from "./DownloadDataButton"; const DAY_HEADERS = [ { text: "D1", dayNumber: 1 }, @@ -51,6 +52,7 @@ const PastWorkouts = ({ updatePastWorkouts }: PastWorkoutsProps) => { {weeklyProgress.length > 0 ? (
+