mirror of
https://github.com/nagaoo0/HabbitGrid.git
synced 2026-01-11 23:44:55 +00:00
Compare commits
9 Commits
android-ve
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 839252e3ef | |||
| a902062726 | |||
| e330346d86 | |||
|
|
b91f94a388 | ||
|
|
99a61b5112 | ||
|
|
87cbfa54f7 | ||
| 2c7d136a33 | |||
| 29bb669f60 | |||
| 85c8aea7d3 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
99
.github/workflows/codeql.yml
vendored
Normal file
99
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL Advanced"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '27 4 * * 6'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze (${{ matrix.language }})
|
||||||
|
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||||
|
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||||
|
# - https://gh.io/supported-runners-and-hardware-resources
|
||||||
|
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||||
|
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||||
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||||
|
permissions:
|
||||||
|
# required for all workflows
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
# required to fetch internal or private CodeQL packs
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
# only required for workflows in private repositories
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: javascript-typescript
|
||||||
|
build-mode: none
|
||||||
|
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||||
|
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||||
|
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||||
|
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||||
|
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||||
|
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||||
|
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||||
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||||
|
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||||
|
# or others). This is typically only required for manual builds.
|
||||||
|
# - name: Setup runtime (example)
|
||||||
|
# uses: actions/setup-example@v1
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
# If the analyze step fails for one of the languages you are analyzing with
|
||||||
|
# "We were unable to automatically build your code", modify the matrix above
|
||||||
|
# to set the build mode to "manual" for that language. Then modify this step
|
||||||
|
# to build your code.
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
- name: Run manual build steps
|
||||||
|
if: matrix.build-mode == 'manual'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||||
|
'languages you are analyzing, replace this with the commands to build' \
|
||||||
|
'your code, for example:'
|
||||||
|
echo ' make bootstrap'
|
||||||
|
echo ' make release'
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
mixaciric000@gmail.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
@@ -1,102 +1,209 @@
|
|||||||
[
|
[
|
||||||
"Great job! Keep going!",
|
"You did it! 🎉",
|
||||||
"You're on fire! 🔥",
|
"High five! ✋",
|
||||||
"Consistency is key!",
|
"You’re awesome!",
|
||||||
"Amazing streak!",
|
"Keep rocking!",
|
||||||
"You crushed it today!",
|
"Level up! 🚀",
|
||||||
"Small steps, big results!",
|
"You’re a legend!",
|
||||||
"Habit hero!",
|
"That’s how it’s done!",
|
||||||
"Progress, not perfection!",
|
"You’re crushing it!",
|
||||||
"Every dot counts!",
|
"Go you!",
|
||||||
"Keep up the momentum!",
|
|
||||||
"You’re building something awesome!",
|
|
||||||
"One step closer to your goal!",
|
|
||||||
"You’re unstoppable!",
|
"You’re unstoppable!",
|
||||||
"Keep the streak alive!",
|
"Boom! Achievement unlocked!",
|
||||||
"You’re making it happen!",
|
"You’re a superstar!",
|
||||||
"Your effort is inspiring!",
|
"You make it look easy!",
|
||||||
"You’re a streak superstar!",
|
"You’re a force of nature!",
|
||||||
"Every day matters!",
|
"You’re a wizard! 🧙♂️",
|
||||||
"You’re a habit legend!",
|
"You’re a ninja! 🥷",
|
||||||
"You’re doing fantastic!",
|
"You’re a rockstar! 🎸",
|
||||||
"Keep shining!",
|
"You’re a champion! 🏆",
|
||||||
"You’re a role model!",
|
"You’re a hero! 🦸♂️",
|
||||||
"You’re a champion!",
|
"You’re a genius!",
|
||||||
"You’re making progress!",
|
"You’re a boss!",
|
||||||
|
"You’re a machine!",
|
||||||
|
"You’re a powerhouse!",
|
||||||
|
"You’re a trailblazer!",
|
||||||
|
"You’re a game changer!",
|
||||||
"You’re a winner!",
|
"You’re a winner!",
|
||||||
"You’re a streak master!",
|
"You’re a motivator!",
|
||||||
"You’re a habit machine!",
|
"You’re a creator!",
|
||||||
"You’re a streak builder!",
|
"You’re a dreamer!",
|
||||||
"You’re a streak star!",
|
"You’re a doer!",
|
||||||
"You’re a streak hero!",
|
"You’re a finisher!",
|
||||||
"You’re a streak ninja!",
|
"You’re a Firestarter!",
|
||||||
"You’re a streak wizard!",
|
"You’re a closer!",
|
||||||
"You’re a streak warrior!",
|
"You’re a believer!",
|
||||||
"You’re a streak explorer!",
|
"You’re a planner!",
|
||||||
"You’re a streak adventurer!",
|
"You’re an organizer!",
|
||||||
"You’re a streak conqueror!",
|
"You’re a strategist!",
|
||||||
"You’re a streak champion!",
|
"You’re a visionary!",
|
||||||
"You’re a streak genius!",
|
"You’re an optimist!",
|
||||||
"You’re a streak guru!",
|
"You’re an enthusiast!",
|
||||||
"You’re a streak expert!",
|
"You’re a player!",
|
||||||
"You’re a streak pro!",
|
"You’re a contender!",
|
||||||
"You’re a streak veteran!",
|
"You’re a competitor!",
|
||||||
"You’re a streak rookie!",
|
"You’re a challenger!",
|
||||||
"You’re a streak all-star!",
|
"You’re a victor!",
|
||||||
"You’re a streak MVP!",
|
"You’re a survivor!",
|
||||||
"You’re a streak superstar!",
|
"You’re a thriver!",
|
||||||
"You’re a streak rockstar!",
|
"You’re an overcomer!",
|
||||||
"You’re a streak dynamo!",
|
"You’re a high achiever!",
|
||||||
"You’re a streak powerhouse!",
|
"Not all those who wander are lost. —J.R.R. Tolkien",
|
||||||
"You’re a streak inspiration!",
|
"So it goes. —Kurt Vonnegut",
|
||||||
"You’re a streak motivator!",
|
"The only way out is through. —Robert Frost",
|
||||||
"You’re a streak leader!",
|
"You have brains in your head. You have feet in your shoes. You can steer yourself any direction you choose. —Dr. Seuss",
|
||||||
"You’re a streak innovator!",
|
"Courage is found in unlikely places. —J.R.R. Tolkien",
|
||||||
"You’re a streak creator!",
|
"Do or do not. There is no try. —Yoda",
|
||||||
"You’re a streak builder!",
|
"It’s dangerous to go alone! Take this. —The Legend of Zelda",
|
||||||
"You’re a streak achiever!",
|
"The game is afoot! —Sherlock Holmes",
|
||||||
"You’re a streak doer!",
|
"To the stars who listen and the dreams that are answered. —Sarah J. Maas",
|
||||||
"You’re a streak finisher!",
|
"All we have to decide is what to do with the time that is given us. —Gandalf",
|
||||||
"You’re a streak starter!",
|
"Even the smallest person can change the course of the future. —Galadriel",
|
||||||
"You’re a streak closer!",
|
"Winter passed and the world grew up. —C.S. Lewis",
|
||||||
"You’re a streak winner!",
|
"The secret of getting ahead is getting started. —Mark Twain",
|
||||||
"You’re a streak believer!",
|
"Not all heroes wear capes.",
|
||||||
"You’re a streak dreamer!",
|
"Achievement unlocked!",
|
||||||
"You’re a streak thinker!",
|
"You rolled a natural 20!",
|
||||||
"You’re a streak planner!",
|
"The odds are ever in your favor.",
|
||||||
"You’re a streak organizer!",
|
"You found the last Horcrux!",
|
||||||
"You’re a streak strategist!",
|
"You solved the puzzle before Watson!",
|
||||||
"You’re a streak tactician!",
|
"You’re the plot twist everyone needed.",
|
||||||
"You’re a streak visionary!",
|
"You’re the missing page in the story.",
|
||||||
"You’re a streak optimist!",
|
"You’re the last piece of the puzzle.",
|
||||||
"You’re a streak realist!",
|
"You’re the answer to the riddle.",
|
||||||
"You’re a streak enthusiast!",
|
"You’re the light at the end of the tunnel.",
|
||||||
"You’re a streak supporter!",
|
"You’re the hero of your own epic.",
|
||||||
"You’re a streak encourager!",
|
"You’re the author of your next chapter.",
|
||||||
"You’re a streak helper!",
|
"You’re the main character energy!",
|
||||||
"You’re a streak friend!",
|
"You’re the chosen one (but in a good way).",
|
||||||
"You’re a streak teammate!",
|
"You’re the unexpected ending everyone loves.",
|
||||||
"You’re a streak partner!",
|
"You’re the plot armor in a tough scene.",
|
||||||
"You’re a streak ally!",
|
"You’re the magic in the mundane.",
|
||||||
"You’re a streak companion!",
|
"You’re the hope in the dystopia.",
|
||||||
"You’re a streak buddy!",
|
"You’re the clever twist in the mystery.",
|
||||||
"You’re a streak pal!",
|
"You’re the spark in the revolution.",
|
||||||
"You’re a streak mate!",
|
"You’re the prophecy fulfilled.",
|
||||||
"You’re a streak peer!",
|
"You’re the legend in the making.",
|
||||||
"You’re a streak colleague!",
|
"You’re the muse for tomorrow’s story.",
|
||||||
"You’re a streak associate!",
|
"You’re the ink in the pen of progress.",
|
||||||
"You’re a streak collaborator!",
|
"You’re the page-turner in a slow chapter.",
|
||||||
"You’re a streak contributor!",
|
"You’re the secret passage in the labyrinth.",
|
||||||
"You’re a streak participant!",
|
"You’re the dragon’s gold at the end of the quest.",
|
||||||
"You’re a streak member!",
|
"You’re the ring-bearer on the journey.",
|
||||||
"You’re a streak player!",
|
"You’re the phoenix rising from the ashes.",
|
||||||
"You’re a streak contender!",
|
"You’re the sword in the stone.",
|
||||||
"You’re a streak competitor!",
|
"You’re the magic bean that grew the beanstalk.",
|
||||||
"You’re a streak challenger!",
|
"You’re the time traveler who fixed the timeline.",
|
||||||
"You’re a streak rival!",
|
"You’re the detective who cracked the case.",
|
||||||
"You’re a streak victor!",
|
"You’re the rebel with a cause.",
|
||||||
"You’re a streak survivor!",
|
"You’re the wizard who remembered the spell.",
|
||||||
"You’re a streak thriver!",
|
"You’re the hobbit who left the Shire.",
|
||||||
"You’re a streak overcomer!",
|
"You’re the poet who found the rhyme.",
|
||||||
"You’re a streak achiever!"
|
"You’re the knight who saved the day.",
|
||||||
|
"You’re the bard who inspired the crowd.",
|
||||||
|
"You’re the scientist who made the breakthrough.",
|
||||||
|
"You’re the explorer who found the map.",
|
||||||
|
"You’re the inventor of your own future.",
|
||||||
|
"You’re the captain of your own starship.",
|
||||||
|
"You’re the one who knocks (success).",
|
||||||
|
"You’re the last Jedi in the room.",
|
||||||
|
"You’re the answer to the ultimate question of life, the universe, and everything.",
|
||||||
|
"You’re the one who remembered the towel. —Hitchhiker’s Guide",
|
||||||
|
"You’re the spark that started the fire.",
|
||||||
|
"You’re the wind beneath the wings.",
|
||||||
|
"You’re the punchline to the cosmic joke.",
|
||||||
|
"You’re the plot device that saves the day.",
|
||||||
|
"You’re the deus ex machina of your own story.",
|
||||||
|
"You’re the twist ending everyone talks about.",
|
||||||
|
"You’re the secret ingredient in the recipe for success.",
|
||||||
|
"You’re the magic feather that lets Dumbo fly.",
|
||||||
|
"You’re the golden ticket in the chocolate bar.",
|
||||||
|
"You’re the red pill in the Matrix.",
|
||||||
|
"You’re the portal to the next adventure.",
|
||||||
|
"You’re the key to the locked door.",
|
||||||
|
"You’re the map to the hidden treasure.",
|
||||||
|
"You’re the light saber in the darkness.",
|
||||||
|
"You’re the spell that works every time.",
|
||||||
|
"You’re the last page in the book—and it’s a happy ending!",
|
||||||
|
"You’re the chosen one, Neo!",
|
||||||
|
"Live long and prosper! 🖖",
|
||||||
|
"To infinity and beyond!",
|
||||||
|
"You’re a wizard, Harry!",
|
||||||
|
"Winter is NOT coming—you’re winning!",
|
||||||
|
"You’ve got the power of Grayskull!",
|
||||||
|
"You’re the hero Gotham deserves!",
|
||||||
|
"Victory tastes sweeter than lembas bread.",
|
||||||
|
"The Sorting Hat would put you in Gryffindor today.",
|
||||||
|
"The cake is not a lie—success is real!",
|
||||||
|
"The odds are ever in your favor.",
|
||||||
|
"Achievement unlocked!",
|
||||||
|
"The Force is strong with this one.",
|
||||||
|
"Elementary, my dear Watson!",
|
||||||
|
"The journey is the reward.",
|
||||||
|
"Allons-y!",
|
||||||
|
"The adventure begins anew.",
|
||||||
|
"The stars look very different today.",
|
||||||
|
"The game is afoot!",
|
||||||
|
"The world is your oyster.",
|
||||||
|
"The pen is mightier—and you’re writing history.",
|
||||||
|
"The quest was perilous, but you prevailed.",
|
||||||
|
"The answer is 42.",
|
||||||
|
"The road goes ever on and on.",
|
||||||
|
"The magic is in the doing.",
|
||||||
|
"The universe just gave you a thumbs up.",
|
||||||
|
"The next chapter is looking epic.",
|
||||||
|
"The finish line is just the beginning.",
|
||||||
|
"The secret passage opened!",
|
||||||
|
"The plot thickens—in your favor.",
|
||||||
|
"The prophecy has been fulfilled.",
|
||||||
|
"The spell worked perfectly.",
|
||||||
|
"The treasure chest is open!",
|
||||||
|
"The portal to greatness is unlocked.",
|
||||||
|
"The time machine landed on success.",
|
||||||
|
"The dragon has been tamed.",
|
||||||
|
"The labyrinth has been solved.",
|
||||||
|
"The riddle is cracked.",
|
||||||
|
"The beacon is lit—Gondor calls for aid!",
|
||||||
|
"The ring is destroyed—peace returns to Middle-earth.",
|
||||||
|
"The Millennium Falcon made the jump to lightspeed.",
|
||||||
|
"The Bat-Signal is shining bright.",
|
||||||
|
"The TARDIS is ready for the next adventure.",
|
||||||
|
"The wand chose the wizard.",
|
||||||
|
"The shield held strong.",
|
||||||
|
"The sword gleams with victory.",
|
||||||
|
"The map led to treasure.",
|
||||||
|
"The spellbook is open to the right page.",
|
||||||
|
"The dice rolled in your favor.",
|
||||||
|
"The stars aligned for you.",
|
||||||
|
"The quest log is complete.",
|
||||||
|
"The story ends happily ever after.",
|
||||||
|
"The legend grows.",
|
||||||
|
"The adventure continues.",
|
||||||
|
"The world just got a little brighter.",
|
||||||
|
"The muse is smiling.",
|
||||||
|
"The universe applauds your effort.",
|
||||||
|
"The crowd goes wild!",
|
||||||
|
"The confetti is falling!",
|
||||||
|
"The fireworks are for you!",
|
||||||
|
"The applause is thunderous!",
|
||||||
|
"The curtain rises on your next act.",
|
||||||
|
"The credits roll—and you’re the star!",
|
||||||
|
"Foundation built, success inevitable. —Isaac Asimov",
|
||||||
|
"The robots would approve your logic. —Isaac Asimov",
|
||||||
|
"Hari Seldon predicted this win! —Isaac Asimov",
|
||||||
|
"You navigated the Sprawl like Case. —William Gibson",
|
||||||
|
"The future is now—cyberspace conquered. —William Gibson",
|
||||||
|
"You’re jacked in and winning. —William Gibson",
|
||||||
|
"The spice must flow—and so does your progress. —Frank Herbert",
|
||||||
|
"Fear is the mind-killer, but you crushed it. —Frank Herbert",
|
||||||
|
"You walked without rhythm and avoided the sandworm. —Frank Herbert",
|
||||||
|
"You claimed the Iron Throne of achievement. —George R.R. Martin",
|
||||||
|
"A mind needs books as a sword needs a whetstone. —George R.R. Martin",
|
||||||
|
"You survived the game of habits—winter is not coming! —George R.R. Martin",
|
||||||
|
"Chaos isn’t a pit, it’s a ladder—and you climbed it. —George R.R. Martin",
|
||||||
|
"Progress is coming. —George R.R. Martin",
|
||||||
|
"You bent the arc of history—psychohistory style. —Isaac Asimov",
|
||||||
|
"You hacked the matrix and rewrote the code. —William Gibson",
|
||||||
|
"You rode the worm to victory. —Frank Herbert",
|
||||||
|
"You played the game of thrones and won. —George R.R. Martin"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://yourdomain.com/</loc>
|
<loc>https://myhabitgrid.com/</loc>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://yourdomain.com/add</loc>
|
<loc>https://myhabitgrid.com/add</loc>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://yourdomain.com/settings</loc>
|
<loc>https://myhabitgrid.com/settings</loc>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://yourdomain.com/login-providers</loc>
|
<loc>https://myhabitgrid.com/login-providers</loc>
|
||||||
</url>
|
</url>
|
||||||
<!-- For dynamic routes like /habit/:id and /edit/:id, add actual URLs if you have them -->
|
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getColorIntensity, isToday, formatDate, getWeekdayLabel, getFrozenDays } from '../lib/utils-habit';
|
import { getColorIntensity, isToday, formatDate, getWeekdayLabel, getFrozenDays, calculateStreaks } from '../lib/utils-habit';
|
||||||
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
||||||
|
|
||||||
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
||||||
@@ -50,6 +50,11 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
|||||||
const cidx = completions.indexOf(dateStr);
|
const cidx = completions.indexOf(dateStr);
|
||||||
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
||||||
habits[idx].completions = completions;
|
habits[idx].completions = completions;
|
||||||
|
// Recalculate streaks so counters reflect immediately
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(completions);
|
||||||
|
const prevRecord = habits[idx].longestStreak || 0;
|
||||||
|
habits[idx].currentStreak = currentStreak;
|
||||||
|
habits[idx].longestStreak = Math.max(longestStreak, prevRecord);
|
||||||
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
||||||
}
|
}
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function getFreezeIcon() {
|
|||||||
return icon || '❄️';
|
return icon || '❄️';
|
||||||
}
|
}
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit';
|
import { getColorIntensity, isToday, formatDate, calculateStreaks } from '../lib/utils-habit';
|
||||||
import { getFrozenDays } from '../lib/utils-habit';
|
import { getFrozenDays } from '../lib/utils-habit';
|
||||||
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
||||||
import { toast } from './ui/use-toast';
|
import { toast } from './ui/use-toast';
|
||||||
@@ -79,6 +79,11 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
|||||||
const cidx = completions.indexOf(dateStr);
|
const cidx = completions.indexOf(dateStr);
|
||||||
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
||||||
habits[idx].completions = completions;
|
habits[idx].completions = completions;
|
||||||
|
// Recalculate streaks locally so counters update immediately
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(completions);
|
||||||
|
const prevRecord = habits[idx].longestStreak || 0;
|
||||||
|
habits[idx].currentStreak = currentStreak;
|
||||||
|
habits[idx].longestStreak = Math.max(longestStreak, prevRecord);
|
||||||
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
||||||
}
|
}
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function ensureUUIDs(habits) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
import { supabase, isSupabaseConfigured } from './supabase';
|
import { supabase, isSupabaseConfigured } from './supabase';
|
||||||
|
import { calculateStreaks } from './utils-habit';
|
||||||
import * as local from './storage';
|
import * as local from './storage';
|
||||||
|
|
||||||
const SYNC_FLAG = 'habitgrid_remote_synced_at';
|
const SYNC_FLAG = 'habitgrid_remote_synced_at';
|
||||||
@@ -30,7 +31,23 @@ const SYNC_FLAG = 'habitgrid_remote_synced_at';
|
|||||||
export const getAuthUser = async () => {
|
export const getAuthUser = async () => {
|
||||||
if (!isSupabaseConfigured()) return null;
|
if (!isSupabaseConfigured()) return null;
|
||||||
const { data } = await supabase.auth.getUser();
|
const { data } = await supabase.auth.getUser();
|
||||||
return data?.user ?? null;
|
const user = data?.user ?? null;
|
||||||
|
// Mark that the user has logged in at least once so we can prompt later if they're logged out
|
||||||
|
try {
|
||||||
|
if (user) localStorage.setItem('habitgrid_ever_logged_in', '1');
|
||||||
|
} catch (e) {
|
||||||
|
// ignore localStorage errors in restrictive environments
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to check whether the user has ever logged in from this browser
|
||||||
|
export const hasEverLoggedIn = () => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem('habitgrid_ever_logged_in') === '1';
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLoggedIn = async () => Boolean(await getAuthUser());
|
export const isLoggedIn = async () => Boolean(await getAuthUser());
|
||||||
@@ -156,7 +173,10 @@ export async function toggleCompletion(habitId, dateStr) {
|
|||||||
const completions = Array.isArray(target.completions) ? [...target.completions] : [];
|
const completions = Array.isArray(target.completions) ? [...target.completions] : [];
|
||||||
const idx = completions.indexOf(dateStr);
|
const idx = completions.indexOf(dateStr);
|
||||||
if (idx > -1) completions.splice(idx, 1); else completions.push(dateStr);
|
if (idx > -1) completions.splice(idx, 1); else completions.push(dateStr);
|
||||||
return updateHabit(habitId, { completions });
|
// Calculate streaks and preserve personal record (do not decrease longest)
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(completions);
|
||||||
|
const nextLongest = Math.max(longestStreak, target.longestStreak || 0);
|
||||||
|
return updateHabit(habitId, { completions, currentStreak, longestStreak: nextLongest });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ async function fetchGitHubEvents({ baseUrl = 'https://api.github.com', username,
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This bullshit is still not working, but at least it is not crashing everything
|
||||||
async function fetchGiteaLike({ baseUrl, username, token }, days = 365) {
|
async function fetchGiteaLike({ baseUrl, username, token }, days = 365) {
|
||||||
let authMode = token ? 'token' : null; // 'token' | 'bearer' | null
|
let authMode = token ? 'token' : null; // 'token' | 'bearer' | null
|
||||||
const counts = {};
|
const counts = {};
|
||||||
@@ -228,6 +229,7 @@ async function fetchGiteaLike({ baseUrl, username, token }, days = 365) {
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//gitlab fetch should work now
|
||||||
async function fetchGitLabEvents({ baseUrl = 'https://gitlab.com', token }, days = 365) {
|
async function fetchGitLabEvents({ baseUrl = 'https://gitlab.com', token }, days = 365) {
|
||||||
const headers = { 'Accept': 'application/json', 'PRIVATE-TOKEN': token };
|
const headers = { 'Accept': 'application/json', 'PRIVATE-TOKEN': token };
|
||||||
const counts = {};
|
const counts = {};
|
||||||
|
|||||||
@@ -126,65 +126,7 @@ export const toggleCompletion = (habitId, dateStr) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
import { getFrozenDays } from './utils-habit.js';
|
import { calculateStreaks } from './utils-habit.js';
|
||||||
const calculateStreaks = (completions) => {
|
|
||||||
if (completions.length === 0) {
|
|
||||||
return { currentStreak: 0, longestStreak: 0 };
|
|
||||||
}
|
|
||||||
// Only use frozen days for streak calculation
|
|
||||||
const frozenDays = getFrozenDays(completions);
|
|
||||||
const allValid = Array.from(new Set([...completions, ...frozenDays]));
|
|
||||||
const sortedDates = allValid
|
|
||||||
.map(d => new Date(d))
|
|
||||||
.sort((a, b) => b - a);
|
|
||||||
|
|
||||||
let currentStreak = 0;
|
|
||||||
let longestStreak = 0;
|
|
||||||
let tempStreak = 1;
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const yesterday = new Date(today);
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
|
|
||||||
const mostRecent = sortedDates[0];
|
|
||||||
mostRecent.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
if (mostRecent.getTime() === today.getTime() || mostRecent.getTime() === yesterday.getTime()) {
|
|
||||||
currentStreak = 1;
|
|
||||||
for (let i = 1; i < sortedDates.length; i++) {
|
|
||||||
const current = new Date(sortedDates[i]);
|
|
||||||
current.setHours(0, 0, 0, 0);
|
|
||||||
const previous = new Date(sortedDates[i - 1]);
|
|
||||||
previous.setHours(0, 0, 0, 0);
|
|
||||||
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
|
||||||
if (diffDays === 1) {
|
|
||||||
currentStreak++;
|
|
||||||
tempStreak++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tempStreak = 1;
|
|
||||||
for (let i = 1; i < sortedDates.length; i++) {
|
|
||||||
const current = new Date(sortedDates[i]);
|
|
||||||
current.setHours(0, 0, 0, 0);
|
|
||||||
const previous = new Date(sortedDates[i - 1]);
|
|
||||||
previous.setHours(0, 0, 0, 0);
|
|
||||||
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
|
||||||
if (diffDays === 1) {
|
|
||||||
tempStreak++;
|
|
||||||
longestStreak = Math.max(longestStreak, tempStreak);
|
|
||||||
} else {
|
|
||||||
tempStreak = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
longestStreak = Math.max(longestStreak, currentStreak, 1);
|
|
||||||
return { currentStreak, longestStreak };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const exportData = () => {
|
export const exportData = () => {
|
||||||
const habits = getHabits();
|
const habits = getHabits();
|
||||||
|
|||||||
@@ -40,6 +40,71 @@ export const getWeekdayLabel = (dayIndex) => {
|
|||||||
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
return labels[dayIndex];
|
return labels[dayIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate current and longest streaks from a list of completion date strings (YYYY-MM-DD)
|
||||||
|
// Rules:
|
||||||
|
// - Streaks count consecutive days
|
||||||
|
// - Today or yesterday must be present to have a non-zero current streak
|
||||||
|
// - We also include "frozen" days (one missed day per month sandwiched by completions)
|
||||||
|
// - Longest streak is at least 1 if there is at least one completion
|
||||||
|
export function calculateStreaks(completions) {
|
||||||
|
if (!Array.isArray(completions) || completions.length === 0) {
|
||||||
|
return { currentStreak: 0, longestStreak: 0 };
|
||||||
|
}
|
||||||
|
// Only use frozen days for streak calculation
|
||||||
|
const frozenDays = getFrozenDays(completions);
|
||||||
|
const allValid = Array.from(new Set([...(completions || []), ...frozenDays]));
|
||||||
|
const sortedDates = allValid
|
||||||
|
.map(d => new Date(d))
|
||||||
|
.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
let currentStreak = 0;
|
||||||
|
let longestStreak = 0;
|
||||||
|
let tempStreak = 1;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
const mostRecent = sortedDates[0];
|
||||||
|
mostRecent.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (mostRecent.getTime() === today.getTime() || mostRecent.getTime() === yesterday.getTime()) {
|
||||||
|
currentStreak = 1;
|
||||||
|
for (let i = 1; i < sortedDates.length; i++) {
|
||||||
|
const current = new Date(sortedDates[i]);
|
||||||
|
current.setHours(0, 0, 0, 0);
|
||||||
|
const previous = new Date(sortedDates[i - 1]);
|
||||||
|
previous.setHours(0, 0, 0, 0);
|
||||||
|
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays === 1) {
|
||||||
|
currentStreak++;
|
||||||
|
tempStreak++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tempStreak = 1;
|
||||||
|
for (let i = 1; i < sortedDates.length; i++) {
|
||||||
|
const current = new Date(sortedDates[i]);
|
||||||
|
current.setHours(0, 0, 0, 0);
|
||||||
|
const previous = new Date(sortedDates[i - 1]);
|
||||||
|
previous.setHours(0, 0, 0, 0);
|
||||||
|
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays === 1) {
|
||||||
|
tempStreak++;
|
||||||
|
longestStreak = Math.max(longestStreak, tempStreak);
|
||||||
|
} else {
|
||||||
|
tempStreak = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
longestStreak = Math.max(longestStreak, currentStreak, 1);
|
||||||
|
return { currentStreak, longestStreak };
|
||||||
|
}
|
||||||
// Returns array of frozen days (date strings) for a given completions array
|
// Returns array of frozen days (date strings) for a given completions array
|
||||||
export function getFrozenDays(completions) {
|
export function getFrozenDays(completions) {
|
||||||
// Map: month string -> frozen day string
|
// Map: month string -> frozen day string
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import HabitCard from '../components/HabitCard';
|
|||||||
import AnimatedCounter from '../components/AnimatedCounter';
|
import AnimatedCounter from '../components/AnimatedCounter';
|
||||||
import GitActivityGrid from '../components/GitActivityGrid';
|
import GitActivityGrid from '../components/GitActivityGrid';
|
||||||
import { getGitEnabled } from '../lib/git';
|
import { getGitEnabled } from '../lib/git';
|
||||||
import { getHabits, updateHabit, syncLocalToRemoteIfNeeded, syncRemoteToLocal, getAuthUser } from '../lib/datastore';
|
import { calculateStreaks } from '../lib/utils-habit';
|
||||||
|
import { getHabits, updateHabit, syncLocalToRemoteIfNeeded, syncRemoteToLocal, getAuthUser, hasEverLoggedIn } from '../lib/datastore';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
@@ -19,6 +20,7 @@ const HomePage = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loggedIn, setLoggedIn] = useState(false);
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
const [collapsedGroups, setCollapsedGroups] = useState({});
|
const [collapsedGroups, setCollapsedGroups] = useState({});
|
||||||
|
const [everLoggedIn, setEverLoggedIn] = useState(false);
|
||||||
const [gitEnabled, setGitEnabled] = useState(getGitEnabled());
|
const [gitEnabled, setGitEnabled] = useState(getGitEnabled());
|
||||||
const [darkMode, setDarkMode] = useState(() => {
|
const [darkMode, setDarkMode] = useState(() => {
|
||||||
return localStorage.getItem('theme') === 'dark';
|
return localStorage.getItem('theme') === 'dark';
|
||||||
@@ -30,6 +32,12 @@ const HomePage = () => {
|
|||||||
// On login, pull remote habits into localStorage
|
// On login, pull remote habits into localStorage
|
||||||
const user = await getAuthUser();
|
const user = await getAuthUser();
|
||||||
setLoggedIn(!!user);
|
setLoggedIn(!!user);
|
||||||
|
// Mark whether this browser has seen a login before
|
||||||
|
try {
|
||||||
|
setEverLoggedIn(hasEverLoggedIn());
|
||||||
|
} catch (e) {
|
||||||
|
setEverLoggedIn(false);
|
||||||
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
await syncRemoteToLocal();
|
await syncRemoteToLocal();
|
||||||
}
|
}
|
||||||
@@ -63,13 +71,27 @@ const HomePage = () => {
|
|||||||
const loadHabits = async () => {
|
const loadHabits = async () => {
|
||||||
// Always read from local for instant UI
|
// Always read from local for instant UI
|
||||||
const loadedHabits = JSON.parse(localStorage.getItem('habitgrid_data') || '[]');
|
const loadedHabits = JSON.parse(localStorage.getItem('habitgrid_data') || '[]');
|
||||||
loadedHabits.sort((a, b) => {
|
// One-time consistency pass: recompute streaks from completions if missing or outdated
|
||||||
|
let changed = false;
|
||||||
|
const updated = loadedHabits.map(h => {
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(h.completions || []);
|
||||||
|
const nextLongest = Math.max(longestStreak, h.longestStreak || 0);
|
||||||
|
if ((h.currentStreak ?? 0) !== currentStreak || (h.longestStreak ?? 0) !== nextLongest) {
|
||||||
|
changed = true;
|
||||||
|
return { ...h, currentStreak, longestStreak: nextLongest };
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
localStorage.setItem('habitgrid_data', JSON.stringify(updated));
|
||||||
|
}
|
||||||
|
updated.sort((a, b) => {
|
||||||
if (a.sortOrder !== undefined && b.sortOrder !== undefined) return a.sortOrder - b.sortOrder;
|
if (a.sortOrder !== undefined && b.sortOrder !== undefined) return a.sortOrder - b.sortOrder;
|
||||||
if (a.sortOrder !== undefined) return -1;
|
if (a.sortOrder !== undefined) return -1;
|
||||||
if (b.sortOrder !== undefined) return 1;
|
if (b.sortOrder !== undefined) return 1;
|
||||||
return new Date(a.createdAt || 0) - new Date(b.createdAt || 0);
|
return new Date(a.createdAt || 0) - new Date(b.createdAt || 0);
|
||||||
});
|
});
|
||||||
setHabits(loadedHabits);
|
setHabits(updated);
|
||||||
// Initialize collapsed state for new categories
|
// Initialize collapsed state for new categories
|
||||||
const categories = Array.from(new Set(loadedHabits.map(h => h.category || 'Uncategorized')));
|
const categories = Array.from(new Set(loadedHabits.map(h => h.category || 'Uncategorized')));
|
||||||
setCollapsedGroups(prev => {
|
setCollapsedGroups(prev => {
|
||||||
@@ -86,7 +108,7 @@ const HomePage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginSync = () => {
|
const handleLoginSync = () => {
|
||||||
navigate('/login');
|
navigate('/login-providers');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManualSync = async () => {
|
const handleManualSync = async () => {
|
||||||
@@ -133,7 +155,21 @@ const HomePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Prompt previously-signed-in users to re-authenticate if currently logged out */}
|
||||||
|
{!loggedIn && everLoggedIn && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="max-w-6xl mx-auto mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/40 rounded-lg border border-yellow-100 dark:border-yellow-800 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="text-sm text-yellow-800 dark:text-yellow-200">Looks like you were previously signed in. Sign in again to keep your habits synced across devices.</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button size="sm" onClick={() => navigate('/login-providers')}>Sign in</Button>
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => { localStorage.removeItem('habitgrid_ever_logged_in'); setEverLoggedIn(false); }}>Dismiss</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Stats Overview */}
|
{/* Stats Overview */}
|
||||||
{habits.length > 0 && (
|
{habits.length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -168,8 +204,6 @@ const HomePage = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Habits List */}
|
{/* Habits List */}
|
||||||
{/* Grouped Habits by Category, collapsible, and uncategorized habits outside */}
|
{/* Grouped Habits by Category, collapsible, and uncategorized habits outside */}
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
@@ -422,8 +456,6 @@ const HomePage = () => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Add Button */}
|
{/* Add Button */}
|
||||||
{habits.length > 0 && (
|
{habits.length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
Reference in New Issue
Block a user