Skip to content

Commit

Permalink
Merge pull request #32 from cjmellor/streak-freeze
Browse files Browse the repository at this point in the history
Adds a feature to freeze a streak
  • Loading branch information
cjmellor committed Aug 21, 2023
2 parents e3466c0 + 09ea680 commit 25c4f21
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 30 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/run-linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: "Run Linter"

on: pull_request

jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Lint with Pint
uses: aglipanci/laravel-pint-action@2.3.0

- name: Commit linted files
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "fix: Files linted with Pint"

53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,59 @@ public Activity $activity,
public Streak $streak,
```

## 🥶 Streak Freezing

Streaks can be frozen, which means they will not be broken if a day is skipped. This is useful for when you want to allow users to take a break from an activity without losing their streak.

The freeze duration is a configurable option in the config file.

```php
'freeze_duration' => env(key: 'STREAK_FREEZE_DURATION', default: 1),
```

### Freeze a Streak

Fetch the activity you want to freeze and pass it to the `freezeStreak` method. A second parameter can be passed to set the duration of the freeze. The default is `1` day (as set in the config)

A `StreakFrozen` Event is ran when a streak is frozen.

```php
$user->freezeStreak(activity: $activity);

$user->freezeStreak(activity: $activity, days: 5); // freeze for 5 days
```

### Unfreeze a Streak

The opposite of freezing a streak is unfreezing it. This will allow the streak to be broken again.

A `StreakUnfrozen` Event is run when a streak is unfrozen.

```php
$user->unfreezeStreak($activity);
```

### Check if a Streak is Frozen

```php
$user->isStreakFrozen($activity);
```

### Events

**StreakFrozen** - When a streak is frozen.

```php
public int $frozenStreakLength,
public Carbon $frozenUntil,
```

**StreakUnfrozen** - When a streak is unfrozen.

```
No data is sent with this event
```

# Testing

```
Expand Down
10 changes: 10 additions & 0 deletions config/level-up.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,14 @@
'archive_streak_history' => [
'enabled' => env(key: 'ARCHIVE_STREAK_HISTORY_ENABLED', default: true),
],

/*
| -------------------------------------------------------------------------
| Default Streak Freeze Time
| -------------------------------------------------------------------------
|
| Set the default time in days that a streak will be frozen for.
|
*/
'freeze_duration' => env(key: 'STREAK_FREEZE_DURATION', default: 1),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
public function up(): void
{
Schema::table('streaks', function (Blueprint $table) {
$table->after('activity_at', function (Blueprint $table) {
$table->timestamp('frozen_until')->nullable();
});
});
}

public function down(): void
{
Schema::table('streaks', function (Blueprint $table) {
$table->dropColumn('frozen_until');
});
}
};
41 changes: 35 additions & 6 deletions src/Concerns/HasStreaks.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Event;
use LevelUp\Experience\Events\StreakBroken;
use LevelUp\Experience\Events\StreakFrozen;
use LevelUp\Experience\Events\StreakIncreased;
use LevelUp\Experience\Events\StreakStarted;
use LevelUp\Experience\Events\StreakUnfroze;
use LevelUp\Experience\Models\Activity;
use LevelUp\Experience\Models\Streak;
use LevelUp\Experience\Models\StreakHistory;
Expand All @@ -18,13 +21,20 @@ public function recordStreak(Activity $activity): void
// If the user doesn't have a streak for this activity, start a new one
if (! $this->hasStreakForActivity(activity: $activity)) {
$this->startNewStreak($activity);

return;
}

$diffInDays = $this->getStreakLastActivity($activity)
->activity_at
->startOfDay()
->diffInDays(now()->startOfDay());

// Checking to see if the streak is frozen
if ($this->getStreakLastActivity($activity)->frozen_until && now()->lessThan($this->getStreakLastActivity($activity)->frozen_until)) {
return;
}

if ($diffInDays === 0) {
return;
}
Expand Down Expand Up @@ -125,10 +135,29 @@ public function hasStreakToday(Activity $activity): bool
->isToday();
}

// public function getLongestStreak(Activity $activity): int
// {
// return $this->streaks()
// ->whereBelongsTo(related: $activity)
// ->max(column: 'count');
// }
public function freezeStreak(Activity $activity, int $days = null): bool
{
$days = $days ?? config(key: 'level-up.freeze_duration');

Event::dispatch(new StreakFrozen(
frozenStreakLength: $days,
frozenUntil: now()->addDays(value: $days)->startOfDay()
));

return $this->getStreakLastActivity($activity)
->update(['frozen_until' => now()->addDays(value: $days)->startOfDay()]);
}

public function unFreezeStreak(Activity $activity): bool
{
Event::dispatch(new StreakUnfroze());

return $this->getStreakLastActivity($activity)
->update(['frozen_until' => null]);
}

public function isStreakFrozen(Activity $activity): bool
{
return ! is_null($this->getStreakLastActivity($activity)->frozen_until);
}
}
14 changes: 14 additions & 0 deletions src/Events/StreakFrozen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace LevelUp\Experience\Events;

use Illuminate\Support\Carbon;

class StreakFrozen
{
public function __construct(
public int $frozenStreakLength,
public Carbon $frozenUntil,
) {
}
}
10 changes: 10 additions & 0 deletions src/Events/StreakUnfroze.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace LevelUp\Experience\Events;

class StreakUnfroze
{
public function __construct()
{
}
}
1 change: 1 addition & 0 deletions src/LevelUpServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function configurePackage(Package $package): void
'create_streak_activities_table',
'create_streaks_table',
'create_streak_histories_table',
'add_streak_freeze_feature_columns_to_streaks_table',
]);
}

Expand Down
1 change: 1 addition & 0 deletions src/Models/Streak.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Streak extends Model

protected $casts = [
'activity_at' => 'datetime',
'frozen_until' => 'datetime',
];

public function user(): BelongsTo
Expand Down
66 changes: 66 additions & 0 deletions tests/Concerns/HasStreaksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

use Illuminate\Support\Facades\Event;
use LevelUp\Experience\Events\StreakBroken;
use LevelUp\Experience\Events\StreakFrozen;
use LevelUp\Experience\Events\StreakIncreased;
use LevelUp\Experience\Events\StreakStarted;
use LevelUp\Experience\Events\StreakUnfroze;
use LevelUp\Experience\Models\Activity;

use function Spatie\PestPluginTestTime\testTime;
Expand Down Expand Up @@ -176,3 +178,67 @@
'ended_at' => now()->subDays(value: 2),
]);
});

test(description: 'a streak can be frozen', closure: function () {
Event::fake();

$this->user->recordStreak($this->activity);

$this->user->freezeStreak($this->activity);

expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay());

Event::assertDispatched(
event: StreakFrozen::class,
callback: fn (StreakFrozen $event): bool => $event->frozenStreakLength === config(key: 'level-up.freeze_duration')
&& $event->frozenUntil->isTomorrow(),
);
});

test(description: 'a streak can be unfrozen', closure: function () {
Event::fake();

$this->user->recordStreak($this->activity);

$this->user->freezeStreak($this->activity);

expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay());

$this->user->unFreezeStreak($this->activity);

expect($this->user->isStreakFrozen($this->activity))->toBeFalse();

Event::assertDispatched(event: StreakUnfroze::class);
});

test(description: 'when a streak is frozen, it does not break', closure: function () {
$this->user->recordStreak($this->activity);

testTime()->addDays();
$this->user->recordStreak($this->activity);

expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 2);

$this->user->freezeStreak($this->activity);

testTime()->addDays();
$this->user->recordStreak($this->activity);

expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 3);
});

test('when a streak is frozen and freeze duration has passed, streak count will reset', function () {
$this->user->recordStreak($this->activity);

testTime()->addDays();
$this->user->recordStreak($this->activity);

expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 2);

$this->user->freezeStreak($this->activity);

testTime()->addDays(2);
$this->user->recordStreak($this->activity);

expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 1);
});
40 changes: 16 additions & 24 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,23 @@ protected function getEnvironmentSetUp($app): void
$table->timestamps();
});

$migration = include __DIR__.'/../database/migrations/create_levels_table.php.stub';
$migration->up();

$migration = include __DIR__.'/../database/migrations/create_experiences_table.php.stub';
$migration->up();

$migration = include __DIR__.'/../database/migrations/add_level_relationship_to_users_table.php.stub';
$migration->up();

$migration = include __DIR__.'/../database/migrations/create_experience_audits_table.php.stub';
$migration->up();

$migration = include __DIR__.'/../database/migrations/create_achievements_table.php.stub';
$migration->up();

$migration = include __DIR__.'/../database/migrations/create_achievement_user_pivot_table.php.stub';
$migration->up();

$migration = include __DIR__.'/../database/migrations/create_streak_activities_table.php.stub';
$migration->up();
$migrationFiles = [
'create_levels_table',
'create_experiences_table',
'add_level_relationship_to_users_table',
'create_experience_audits_table',
'create_achievements_table',
'create_achievement_user_pivot_table',
'create_streak_activities_table',
'create_streaks_table',
'create_streak_histories_table',
'add_streak_freeze_feature_columns_to_streaks_table',
];

$migration = include __DIR__.'/../database/migrations/create_streaks_table.php.stub';
$migration->up();
foreach ($migrationFiles as $migrationFile) {
$migration = include __DIR__."/../database/migrations/$migrationFile.php.stub";

$migration = include __DIR__.'/../database/migrations/create_streak_histories_table.php.stub';
$migration->up();
$migration->up();
}
}
}

0 comments on commit 25c4f21

Please sign in to comment.