diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index f926c354..1fe49971 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -23,6 +23,7 @@ class UserController extends Controller { /** @var User */ $user = auth()->user(); + return $user ->makeHidden(['password', 'ip', 'remember_token', 'verification_token']); } @@ -31,11 +32,11 @@ class UserController extends Controller { $user = Auth::user(); - [$from, $to] = explode(',', option('sign_score')); + [$min, $max] = explode(',', option('sign_score')); $scoreIntro = trans('user.score-intro.introduction', [ 'initial_score' => option('user_initial_score'), - 'score-from' => $from, - 'score-to' => $to, + 'score-from' => $min, + 'score-to' => $max, 'return-score' => option('return_score') ? trans('user.score-intro.will-return-score') : trans('user.score-intro.no-return-score'), @@ -60,10 +61,6 @@ class UserController extends Controller $parsedown = new Parsedown(); return view('user.index')->with([ - 'statistics' => [ - 'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')), - 'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')), - ], 'score_intro' => $scoreIntro, 'rates' => [ 'storage' => option('score_per_storage'), @@ -80,89 +77,55 @@ class UserController extends Controller { $user = Auth::user(); - return json('', 0, [ + return response()->json([ 'user' => [ 'score' => $user->score, 'lastSignAt' => $user->last_sign_at, ], - 'stats' => [ - 'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')), - 'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')), + 'rate' => [ + 'storage' => (int) option('score_per_storage'), + 'players' => (int) option('score_per_player'), + ], + 'usage' => [ + 'players' => $user->players->count(), + 'storage' => (int) Texture::where('uploader', $user->uid)->sum('size'), ], 'signAfterZero' => (bool) option('sign_after_zero'), 'signGapTime' => (int) option('sign_gap_time'), ]); } - protected function calculatePercentageUsed(int $used, int $rate): array - { - $user = Auth::user(); - // Initialize default value to avoid division by zero. - $result['used'] = $used; - $result['total'] = 'UNLIMITED'; - $result['percentage'] = 0; - - if ($rate != 0) { - $result['total'] = $used + floor($user->score / $rate); - $result['percentage'] = $result['total'] ? $used / $result['total'] * 100 : 100; - } - - return $result; - } - - protected function getStorageUsed(User $user) - { - return Texture::where('uploader', $user->uid)->select('size')->sum('size'); - } - public function sign() { + /** @var User */ $user = Auth::user(); - if ($this->getSignRemainingTime($user) <= 0) { - $acquiredScore = rand(...explode(',', option('sign_score'))); + + $lastSignTime = Carbon::parse($user->last_sign_at); + $remainingTime = option('sign_after_zero') + ? Carbon::now()->diffInSeconds( + $lastSignTime <= Carbon::today() ? $lastSignTime : Carbon::tomorrow(), + false + ) + : Carbon::now()->diffInSeconds( + $lastSignTime->addHours((int) option('sign_gap_time')), + false + ); + + if ($remainingTime <= 0) { + [$min, $max] = explode(',', option('sign_score')); + $acquiredScore = rand((int) $min, (int) $max); $user->score += $acquiredScore; $user->last_sign_at = Carbon::now(); $user->save(); - $gap = option('sign_gap_time'); return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [ 'score' => $user->score, - 'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')), - 'remaining_time' => $gap > 1 ? round($gap) : $gap, ]); } else { - $remaining_time = $this->getUserSignRemainingTimeWithPrecision($user); - - return json(trans('user.cant-sign-until', [ - 'time' => $remaining_time >= 1 - ? $remaining_time : round($remaining_time * 60), - 'unit' => $remaining_time >= 1 - ? trans('user.time-unit-hour') : trans('user.time-unit-min'), - ]), 1); + return json('', 1); } } - protected function getUserSignRemainingTimeWithPrecision(User $user) - { - $hours = $this->getSignRemainingTime($user) / 3600; - - return $hours > 1 ? round($hours) : $hours; - } - - protected function getSignRemainingTime(User $user) - { - $lastSignTime = Carbon::parse($user->last_sign_at); - - if (option('sign_after_zero')) { - return Carbon::now()->diffInSeconds( - $lastSignTime <= Carbon::today() ? $lastSignTime : Carbon::tomorrow(), - false - ); - } - - return Carbon::now()->diffInSeconds($lastSignTime->addHours(option('sign_gap_time')), false); - } - public function sendVerificationEmail() { if (!option('require_verification')) { @@ -187,7 +150,6 @@ class UserController extends Controller try { Mail::to($user->email)->send(new EmailVerification(url($url))); } catch (\Exception $e) { - // Write the exception to log report($e); return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2); @@ -231,6 +193,7 @@ class UserController extends Controller public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher) { $action = $request->input('action', ''); + /** @var User */ $user = Auth::user(); $addition = $request->except('action'); @@ -333,6 +296,7 @@ class UserController extends Controller { $request->validate(['tid' => 'required|integer']); $tid = $request->input('tid'); + /** @var User */ $user = auth()->user(); $can = $filter->apply('user_can_update_avatar', true, [$user, $tid]); diff --git a/resources/assets/src/views/user/Dashboard/InfoBox.tsx b/resources/assets/src/views/user/Dashboard/InfoBox.tsx index 206e27e0..5f4836f2 100644 --- a/resources/assets/src/views/user/Dashboard/InfoBox.tsx +++ b/resources/assets/src/views/user/Dashboard/InfoBox.tsx @@ -5,12 +5,13 @@ interface Props { icon: string color: string used: number - total: number + unused: number unit: string } const InfoBox: React.FC = (props) => { - const percentage = (props.used / props.total) * 100 + const total = ~~(props.used + props.unused) + const percentage = (props.used / total) * 100 return (
@@ -20,7 +21,7 @@ const InfoBox: React.FC = (props) => {
{props.name} - {props.used} / {props.total} {props.unit} + {props.used} / {total} {props.unit}
diff --git a/resources/assets/src/views/user/Dashboard/index.tsx b/resources/assets/src/views/user/Dashboard/index.tsx index d5a94905..11d8f2bb 100644 --- a/resources/assets/src/views/user/Dashboard/index.tsx +++ b/resources/assets/src/views/user/Dashboard/index.tsx @@ -9,22 +9,18 @@ import urls from '@/scripts/urls' import * as breakpoints from '@/styles/breakpoints' import InfoBox from './InfoBox' import SignButton from './SignButton' +import * as scoreUtils from './scoreUtils' type ScoreInfo = { signAfterZero: boolean signGapTime: number - stats: { players: Stat; storage: Stat } + rate: { players: number; storage: number } + usage: { players: number; storage: number } user: { score: number; lastSignAt: string } } -type Stat = { - used: number - total: number -} - type SignReturn = { score: number - storage: Stat } const ScoreTitle = styled.p` @@ -47,9 +43,12 @@ const ScoreNotice = styled.p` const Dashboard: React.FC = () => { const [loading, setLoading] = useState(false) - const [players, setPlayers] = useState({ used: 0, total: 1 }) - const [storage, setStorage] = useState({ used: 0, total: 1 }) - const [score, setScore] = useTween(0) + const [players, setPlayers] = useState(0) + const [storage, setStorage] = useState(0) + const [score, setScore] = useState(0) + const [tweenedScore, setTweenedScore] = useTween(0) + const [playersRate, setPlayersRate] = useState(1) + const [storageRate, setStorageRate] = useState(1) const [lastSign, setLastSign] = useState(new Date()) const [canSignAfterZero, setCanSignAfterZero] = useState(false) const [signGap, setSignGap] = useState(24) @@ -57,12 +56,13 @@ const Dashboard: React.FC = () => { useEffect(() => { const fetchInfo = async () => { setLoading(true) - const { data } = await fetch.get>( - urls.user.score(), - ) - setPlayers(data.stats.players) - setStorage(data.stats.storage) + const data = await fetch.get(urls.user.score()) + setPlayers(data.usage.players) + setStorage(data.usage.storage) + setTweenedScore(data.user.score) setScore(data.user.score) + setPlayersRate(data.rate.players) + setStorageRate(data.rate.storage) setLastSign(new Date(data.user.lastSignAt)) setCanSignAfterZero(data.signAfterZero) setSignGap(data.signGapTime) @@ -80,10 +80,15 @@ const Dashboard: React.FC = () => { if (code === 0) { toast.success(message) setLastSign(new Date()) + setTweenedScore(data.score) setScore(data.score) - setStorage(data.storage) } else { - toast.warning(message) + const remainingTime = scoreUtils.remainingTime( + lastSign, + signGap, + canSignAfterZero, + ) + toast.warning(scoreUtils.remainingTimeText(remainingTime)) } setLoading(false) }, []) @@ -101,17 +106,17 @@ const Dashboard: React.FC = () => { color="teal" icon="gamepad" name={t('user.used.players')} - used={players.used} - total={players.total} + used={players} + unused={score / playersRate} unit="" /> - {storage.used > 1024 ? ( + {storage > 1024 ? ( ) : ( @@ -119,8 +124,8 @@ const Dashboard: React.FC = () => { color="maroon" icon="hdd" name={t('user.used.storage')} - used={storage.used} - total={storage.total} + used={storage} + unused={score / storageRate} unit="KB" /> )} @@ -128,7 +133,7 @@ const Dashboard: React.FC = () => {
{t('user.cur-score')} - {~~score} + {~~tweenedScore} {t('user.score-notice')}
diff --git a/resources/assets/src/views/user/Dashboard/scoreUtils.ts b/resources/assets/src/views/user/Dashboard/scoreUtils.ts index 99e95bf8..0014f301 100644 --- a/resources/assets/src/views/user/Dashboard/scoreUtils.ts +++ b/resources/assets/src/views/user/Dashboard/scoreUtils.ts @@ -23,11 +23,11 @@ export function remainingTime( export function remainingTimeText(remainingTime: number): string { const time = remainingTime / ONE_MINUTE return time < 60 - ? t('user.sign-remain-time', { + ? t('user.signRemainingTime', { time: ~~time, unit: t('user.time-unit-min'), }) - : t('user.sign-remain-time', { + : t('user.signRemainingTime', { time: ~~(time / 60), unit: t('user.time-unit-hour'), }) diff --git a/resources/assets/tests/views/user/Dashboard.test.tsx b/resources/assets/tests/views/user/Dashboard.test.tsx index 5bc81457..202bf3af 100644 --- a/resources/assets/tests/views/user/Dashboard.test.tsx +++ b/resources/assets/tests/views/user/Dashboard.test.tsx @@ -4,41 +4,42 @@ import * as fetch from '@/scripts/net' import { t } from '@/scripts/i18n' import urls from '@/scripts/urls' import Dashboard from '@/views/user/Dashboard' +import * as scoreUtils from '@/views/user/Dashboard/scoreUtils' jest.mock('@/scripts/net') -function scoreInfo(data = {}, user = {}, stats = {}) { +function scoreInfo(data = {}, user = {}, usage = {}) { return { - data: { - user: { score: 600, lastSignAt: '2018-08-07 16:06:49', ...user }, - stats: { - players: { used: 3, total: 15 }, - storage: { used: 5, total: 20 }, - ...stats, - }, - signAfterZero: false, - signGapTime: '24', - ...data, + user: { score: 600, lastSignAt: '2018-08-07 16:06:49', ...user }, + usage: { + players: 3, + storage: 5, + ...usage, }, + rate: { + players: 10, + storage: 1, + }, + signAfterZero: false, + signGapTime: '24', + ...data, } } describe('info box', () => { it('players', async () => { - fetch.get.mockResolvedValue( - scoreInfo({}, { score: 0 }, { players: { used: 13, total: 21 } }), - ) + fetch.get.mockResolvedValue(scoreInfo({}, { score: 40 })) const { getByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) - expect(getByText('13')).toBeInTheDocument() - expect(getByText(/21/)).toBeInTheDocument() + expect(getByText('3')).toBeInTheDocument() + expect(getByText(/7/)).toBeInTheDocument() }) describe('storage', () => { it('in KB', async () => { fetch.get.mockResolvedValue( - scoreInfo({}, { score: 0 }, { storage: { used: 700, total: 800 } }), + scoreInfo({}, { score: 100 }, { storage: 700 }), ) const { getByText } = render() @@ -50,13 +51,13 @@ describe('info box', () => { it('in MB', async () => { fetch.get.mockResolvedValue( - scoreInfo({}, { score: 0 }, { storage: { used: 7168, total: 10240 } }), + scoreInfo({}, { score: 3072 }, { storage: 4096 }), ) const { getByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) - expect(getByText('7')).toBeInTheDocument() - expect(getByText(/10/)).toBeInTheDocument() + expect(getByText('4')).toBeInTheDocument() + expect(getByText(/7/)).toBeInTheDocument() expect(getByText(/MB/)).toBeInTheDocument() }) }) @@ -67,11 +68,11 @@ describe('sign', () => { fetch.get.mockResolvedValue(scoreInfo()) }) - it('should succeed', async () => { + it('succeeded', async () => { fetch.post.mockResolvedValue({ code: 0, message: 'ok', - data: { score: 900, storage: { used: 5, total: 25 } }, + data: { score: 900 }, }) const { getByRole, getByText, queryByText } = render() @@ -83,18 +84,22 @@ describe('sign', () => { expect(getByText('ok')).toBeInTheDocument() expect(getByRole('status')).toHaveClass('alert-success') expect(button).toBeDisabled() - expect(queryByText(/25/)).toBeInTheDocument() + expect(queryByText(/905/)).toBeInTheDocument() }) - it('should fail', async () => { - fetch.post.mockResolvedValue({ code: 1, message: 'f', data: {} }) + it('failed', async () => { + fetch.post.mockResolvedValue({ code: 1, message: '' }) const { getByRole, getByText } = render() await waitFor(() => expect(fetch.get).toBeCalledTimes(1)) fireEvent.click(getByRole('button')) await waitFor(() => expect(fetch.post).toBeCalledWith(urls.user.sign())) - expect(getByText('f')).toBeInTheDocument() + + const remainingTime = 24 * 3600 * 1000 + expect( + getByText(scoreUtils.remainingTimeText(remainingTime)), + ).toBeInTheDocument() expect(getByRole('alert')).toHaveClass('alert-warning') }) }) diff --git a/resources/lang/en/user.yml b/resources/lang/en/user.yml index 5060b713..def53a0f 100644 --- a/resources/lang/en/user.yml +++ b/resources/lang/en/user.yml @@ -7,11 +7,6 @@ cur-score: Current Score score-notice: Click the score to show introduction. sign: Sign sign-success: Signed successfully. You got :score scores. -time-unit-hour: h -time-unit-min: min -cant-sign-until: You can't sign in in :time :unit -last-sign: Last signed at :time -sign-remain-time: Available after :time :unit announcement: Announcement no-unread: No new notifications. diff --git a/tests/HttpTest/ControllersTest/UserControllerTest.php b/tests/HttpTest/ControllersTest/UserControllerTest.php index 46eb15a7..fed56e96 100644 --- a/tests/HttpTest/ControllersTest/UserControllerTest.php +++ b/tests/HttpTest/ControllersTest/UserControllerTest.php @@ -34,11 +34,10 @@ class UserControllerTest extends TestCase $uid = $user->uid; factory(\App\Models\Player::class)->create(['uid' => $uid]); + $announcement = (new Parsedown())->text(option_localized('announcement')); $this->actingAs($user) ->get('/user') - ->assertViewHas('statistics') - ->assertSee((new Parsedown())->text(option_localized('announcement')), false) - ->assertSee((string) $user->score); + ->assertSee($announcement, false); $filter->assertApplied('grid:user.index'); $filter->assertApplied('user_avatar', function ($url, $user) use ($uid) { $this->assertTrue(Str::contains($url, '/avatar/user/'.$uid)); @@ -60,26 +59,22 @@ class UserControllerTest extends TestCase $this->actingAs($user) ->get('/user/score-info') - ->assertJson(['data' => [ + ->assertJson([ 'user' => [ 'score' => $user->score, 'lastSignAt' => $user->last_sign_at, ], - 'stats' => [ - 'players' => [ - 'used' => 1, - 'total' => 11, - 'percentage' => 1 / 11 * 100, - ], - 'storage' => [ - 'used' => 0, - 'total' => $user->score, - 'percentage' => 0, - ], + 'rate' => [ + 'storage' => (int) option('score_per_storage'), + 'players' => (int) option('score_per_player'), + ], + 'usage' => [ + 'players' => 1, + 'storage' => 0, ], 'signAfterZero' => option('sign_after_zero'), 'signGapTime' => option('sign_gap_time'), - ]]); + ]); } public function testSign() @@ -87,7 +82,7 @@ class UserControllerTest extends TestCase option(['sign_score' => '50,50']); $user = factory(User::class)->create(); - // Success + // success $this->actingAs($user) ->postJson('/user/sign') ->assertJson([ @@ -95,57 +90,25 @@ class UserControllerTest extends TestCase 'message' => trans('user.sign-success', ['score' => 50]), 'data' => [ 'score' => option('user_initial_score') + 50, - 'storage' => [ - 'percentage' => 0, - 'total' => option('user_initial_score') + 50, - 'used' => 0, - ], - 'remaining_time' => (int) option('sign_gap_time'), ], ]); - // Remaining time is greater than 0 + // remaining time is greater than 0 $user = factory(User::class)->create(['last_sign_at' => Carbon::now()]); option(['sign_gap_time' => 2]); $this->actingAs($user) ->postJson('/user/sign') - ->assertJson([ - 'code' => 1, - 'message' => trans( - 'user.cant-sign-until', - [ - 'time' => 2, - 'unit' => trans('user.time-unit-hour'), - ] - ), - ]); + ->assertJson(['code' => 1]); - // Can sign after 0 o'clock + // can sign after 0 o'clock option(['sign_after_zero' => true]); $user = factory(User::class)->create(['last_sign_at' => Carbon::now()]); - $diff = \Carbon\Carbon::now()->diffInSeconds(\Carbon\Carbon::tomorrow()); - if ($diff / 3600 >= 1) { - $diff = round($diff / 3600); - $unit = 'hour'; - } else { - $diff = round($diff / 60); - $unit = 'min'; - } $this->actingAs($user) ->postJson('/user/sign') - ->assertJson([ - 'code' => 1, - 'message' => trans( - 'user.cant-sign-until', - [ - 'time' => $diff, - 'unit' => trans("user.time-unit-$unit"), - ] - ), - ]); + ->assertJson(['code' => 1]); $user = factory(User::class)->create([ - 'last_sign_at' => \Carbon\Carbon::today(), + 'last_sign_at' => Carbon::today(), ]); $this->actingAs($user)->postJson('/user/sign')->assertJson(['code' => 0]); }