Compare commits

3 Commits
dev ... main

Author SHA1 Message Date
efdf646787 Merge pull request 'ci(deploy): fix mounted host path' (#3) from dev into main
Some checks failed
Deploy / deploy (push) Failing after 1s
2026-03-26 18:40:41 +00:00
705ba6bbd8 Merge pull request 'ci: add eslint, vitest coverage, improved CI and deploy workflows' (#2) from dev into main
Some checks failed
Deploy / deploy (push) Failing after 1s
2026-03-26 18:35:21 +00:00
2db0e6d6e8 Merge pull request 'feat: Модуль Финансы + Трекер + CI/CD' (#1) from dev into main
All checks were successful
Deploy Production / deploy (push) Successful in 26s
Reviewed-on: #1
2026-03-01 05:14:59 +00:00
71 changed files with 50 additions and 32517 deletions

View File

@@ -20,12 +20,12 @@ jobs:
run: npm ci
- name: Lint
run: ESLINT_USE_FLAT_CONFIG=false npx eslint src/ --max-warnings 0
continue-on-error: true
run: npx eslint src/ --max-warnings 0
- name: Test
run: npx vitest run --reporter=verbose
run: npx vitest run --coverage --reporter=verbose
- name: Coverage Check
run: npx vitest run --coverage 2>&1 | tee coverage-output.txt
continue-on-error: true
run: |
npx vitest run --coverage 2>&1 | tee coverage-output.txt
echo "Coverage report generated"

View File

@@ -8,17 +8,19 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy pulse-web
run: |
# Runner mounts docker.sock, so we can control host Docker
docker compose -f /opt/digital-home/pulse-web/docker-compose.yml up -d --build
echo 'Waiting for container...'
cd /opt/digital-home/pulse-web
docker compose up -d --build
echo "Waiting for container..."
sleep 5
STATUS=$(docker inspect --format='{{.State.Status}}' pulse-web 2>/dev/null || echo 'unknown')
STATUS=$(docker inspect --format='{{.State.Status}}' pulse-web 2>/dev/null || echo "unknown")
echo "Container status: $STATUS"
if [ "$STATUS" != 'running' ]; then
echo '::error::Container is not running after deploy'
if [ "$STATUS" != "running" ]; then
echo "::error::Container is not running after deploy"
docker logs pulse-web --tail=20
exit 1
fi
echo 'Deploy successful!'
echo "Deploy successful!"

View File

@@ -1,226 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for api/finance.js</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> finance.js</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>19/19</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>10/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>19/19</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import client from './client'
&nbsp;
export const financeApi = {
// Categories
listCategories: async () =&gt; {
const res = await client.get('finance/categories')
return res.data
},
createCategory: async (data) =&gt; {
const res = await client.post('finance/categories', data)
return res.data
},
updateCategory: async (id, data) =&gt; {
const res = await client.put(`finance/categories/${id}`, data)
return res.data
},
deleteCategory: async (id) =&gt; {
await client.delete(`finance/categories/${id}`)
},
&nbsp;
// Transactions
listTransactions: async (params = {}) =&gt; {
const res = await client.get('finance/transactions', { params })
return res.data
},
createTransaction: async (data) =&gt; {
const res = await client.post('finance/transactions', data)
return res.data
},
updateTransaction: async (id, data) =&gt; {
const res = await client.put(`finance/transactions/${id}`, data)
return res.data
},
deleteTransaction: async (id) =&gt; {
await client.delete(`finance/transactions/${id}`)
},
&nbsp;
// Summary &amp; Analytics
getSummary: async (params = {}) =&gt; {
const res = await client.get('finance/summary', { params })
return res.data
},
getAnalytics: async (params = {}) =&gt; {
const res = await client.get('finance/analytics', { params })
return res.data
},
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,148 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for api/habits.js</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> habits.js</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">91.66% </span>
<span class="quiet">Statements</span>
<span class='fraction'>22/24</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>2/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">91.3% </span>
<span class="quiet">Functions</span>
<span class='fraction'>21/23</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">92.85% </span>
<span class="quiet">Lines</span>
<span class='fraction'>13/14</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import api from './client'
&nbsp;
export const habitsApi = {
list: () =&gt; api.get('/habits').then(r =&gt; r.data),
get: <span class="fstat-no" title="function not covered" >(i</span>d) =&gt; <span class="cstat-no" title="statement not covered" >api.get(`/habits/${id}`).then(<span class="fstat-no" title="function not covered" >r </span>=&gt; <span class="cstat-no" title="statement not covered" >r.data)</span>,</span>
create: (data) =&gt; api.post('/habits', data).then(r =&gt; r.data),
update: (id, data) =&gt; api.put(`/habits/${id}`, data).then(r =&gt; r.data),
delete: (id) =&gt; api.delete(`/habits/${id}`),
log: (id, data = {}) =&gt; api.post(`/habits/${id}/log`, data).then(r =&gt; r.data),
getLogs: (id, days = 30) =&gt; api.get(`/habits/${id}/logs?days=${days}`).then(r =&gt; r.data),
deleteLog: (habitId, logId) =&gt; api.delete(`/habits/${habitId}/logs/${logId}`),
getStats: () =&gt; api.get('/habits/stats').then(r =&gt; r.data),
getHabitStats: (id) =&gt; api.get(`/habits/${id}/stats`).then(r =&gt; r.data),
&nbsp;
// Freezes
getFreezes: (habitId) =&gt; api.get(`/habits/${habitId}/freezes`).then(r =&gt; r.data),
addFreeze: (habitId, data) =&gt; api.post(`/habits/${habitId}/freezes`, data).then(r =&gt; r.data),
deleteFreeze: (habitId, freezeId) =&gt; api.delete(`/habits/${habitId}/freezes/${freezeId}`),
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,176 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for api</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> api</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">89.1% </span>
<span class="quiet">Statements</span>
<span class='fraction'>90/101</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>12/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">84.93% </span>
<span class="quiet">Functions</span>
<span class='fraction'>62/73</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90% </span>
<span class="quiet">Lines</span>
<span class='fraction'>72/80</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="finance.js"><a href="finance.js.html">finance.js</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="19" class="abs high">19/19</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="19" class="abs high">19/19</td>
</tr>
<tr>
<td class="file high" data-value="habits.js"><a href="habits.js.html">habits.js</a></td>
<td data-value="91.66" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 91%"></div><div class="cover-empty" style="width: 9%"></div></div>
</td>
<td data-value="91.66" class="pct high">91.66%</td>
<td data-value="24" class="abs high">22/24</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="91.3" class="pct high">91.3%</td>
<td data-value="23" class="abs high">21/23</td>
<td data-value="92.85" class="pct high">92.85%</td>
<td data-value="14" class="abs high">13/14</td>
</tr>
<tr>
<td class="file high" data-value="profile.js"><a href="profile.js.html">profile.js</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
</tr>
<tr>
<td class="file medium" data-value="savings.js"><a href="savings.js.html">savings.js</a></td>
<td data-value="73.52" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 73%"></div><div class="cover-empty" style="width: 27%"></div></div>
</td>
<td data-value="73.52" class="pct medium">73.52%</td>
<td data-value="34" class="abs medium">25/34</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="4" class="abs high">4/4</td>
<td data-value="70" class="pct medium">70%</td>
<td data-value="30" class="abs medium">21/30</td>
<td data-value="69.56" class="pct medium">69.56%</td>
<td data-value="23" class="abs medium">16/23</td>
</tr>
<tr>
<td class="file high" data-value="tasks.js"><a href="tasks.js.html">tasks.js</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="19" class="abs high">19/19</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="19" class="abs high">19/19</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,127 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for api/profile.js</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> profile.js</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>5/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>2/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>5/5</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import api from "./client"
&nbsp;
export const profileApi = {
get: async () =&gt; {
const { data } = await api.get("/profile")
return data
},
update: async (profileData) =&gt; {
const { data } = await api.put("/profile", profileData)
return data
},
}
&nbsp;
export default profileApi
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,235 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for api/savings.js</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> savings.js</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">73.52% </span>
<span class="quiet">Statements</span>
<span class='fraction'>25/34</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>4/4</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">70% </span>
<span class="quiet">Functions</span>
<span class='fraction'>21/30</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">69.56% </span>
<span class="quiet">Lines</span>
<span class='fraction'>16/23</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import api from "./client"
&nbsp;
export const savingsApi = {
// Categories
listCategories: () =&gt; api.get("/savings/categories").then((r) =&gt; r.data),
getCategory: (id) =&gt; api.get(`/savings/categories/${id}`).then((r) =&gt; r.data),
createCategory: (data) =&gt; api.post("/savings/categories", data).then((r) =&gt; r.data),
updateCategory: (id, data) =&gt;
api.put(`/savings/categories/${id}`, data).then((r) =&gt; r.data),
deleteCategory: (id) =&gt; api.delete(`/savings/categories/${id}`),
&nbsp;
// Transactions
listTransactions: (categoryId, limit = 100, offset = 0) =&gt; {
let url = `/savings/transactions?limit=${limit}&amp;offset=${offset}`
if (categoryId) url += `&amp;category_id=${categoryId}`
return api.get(url).then((r) =&gt; r.data)
},
createTransaction: (data) =&gt;
api.post("/savings/transactions", data).then((r) =&gt; r.data),
updateTransaction: <span class="fstat-no" title="function not covered" >(i</span>d, data) =&gt;
<span class="cstat-no" title="statement not covered" > api.put(`/savings/transactions/${id}`, data).then(<span class="fstat-no" title="function not covered" >(r</span>) =&gt; <span class="cstat-no" title="statement not covered" >r.data)</span>,</span>
deleteTransaction: <span class="fstat-no" title="function not covered" >(i</span>d) =&gt; <span class="cstat-no" title="statement not covered" >api.delete(`/savings/transactions/${id}`),</span>
&nbsp;
// Stats
getStats: () =&gt; api.get("/savings/stats").then((r) =&gt; r.data),
&nbsp;
// Members
getMembers: (categoryId) =&gt;
api.get(`/savings/categories/${categoryId}/members`).then((r) =&gt; r.data),
addMember: (categoryId, userId) =&gt;
api
.post(`/savings/categories/${categoryId}/members`, { user_id: userId })
.then((r) =&gt; r.data),
removeMember: <span class="fstat-no" title="function not covered" >(c</span>ategoryId, userId) =&gt;
<span class="cstat-no" title="statement not covered" > api.delete(`/savings/categories/${categoryId}/members/${userId}`),</span>
&nbsp;
// Recurring Plans
getRecurringPlans: (categoryId) =&gt;
api
.get(`/savings/categories/${categoryId}/recurring-plans`)
.then((r) =&gt; r.data),
createRecurringPlan: <span class="fstat-no" title="function not covered" >(c</span>ategoryId, data) =&gt;
<span class="cstat-no" title="statement not covered" > api</span>
.post(`/savings/categories/${categoryId}/recurring-plans`, data)
.then(<span class="fstat-no" title="function not covered" >(r</span>) =&gt; <span class="cstat-no" title="statement not covered" >r.data)</span>,
updateRecurringPlan: <span class="fstat-no" title="function not covered" >(p</span>lanId, data) =&gt;
<span class="cstat-no" title="statement not covered" > api.put(`/savings/recurring-plans/${planId}`, data).then(<span class="fstat-no" title="function not covered" >(r</span>) =&gt; <span class="cstat-no" title="statement not covered" >r.data)</span>,</span>
deleteRecurringPlan: <span class="fstat-no" title="function not covered" >(p</span>lanId) =&gt;
<span class="cstat-no" title="statement not covered" > api.delete(`/savings/recurring-plans/${planId}`),</span>
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,223 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for api/tasks.js</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> tasks.js</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>19/19</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>19/19</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import client from './client'
&nbsp;
export const tasksApi = {
list: async (completed = null) =&gt; {
let url = 'tasks'
if (completed !== null) {
url += `?completed=${completed}`
}
const res = await client.get(url)
return res.data
},
&nbsp;
today: async () =&gt; {
const res = await client.get('tasks/today')
return res.data
},
&nbsp;
get: async (id) =&gt; {
const res = await client.get(`tasks/${id}`)
return res.data
},
&nbsp;
create: async (data) =&gt; {
const res = await client.post('tasks', data)
return res.data
},
&nbsp;
update: async (id, data) =&gt; {
const res = await client.put(`tasks/${id}`, data)
return res.data
},
&nbsp;
delete: async (id) =&gt; {
await client.delete(`tasks/${id}`)
},
&nbsp;
complete: async (id) =&gt; {
const res = await client.post(`tasks/${id}/complete`)
return res.data
},
&nbsp;
uncomplete: async (id) =&gt; {
const res = await client.post(`tasks/${id}/uncomplete`)
return res.data
},
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,224 +0,0 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View File

@@ -1,87 +0,0 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selector that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,712 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components/LogHabitModal.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">components</a> LogHabitModal.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">58.33% </span>
<span class="quiet">Statements</span>
<span class='fraction'>28/48</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">56.52% </span>
<span class="quiet">Branches</span>
<span class='fraction'>26/46</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">50% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/16</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">64.28% </span>
<span class="quiet">Lines</span>
<span class='fraction'>27/42</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">155x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">35x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">30x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">155x</span>
<span class="cline-any cline-yes">155x</span>
<span class="cline-any cline-yes">155x</span>
<span class="cline-any cline-yes">155x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">155x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState, useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X, ChevronLeft, ChevronRight, Check } from 'lucide-react'
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isFuture, startOfDay, subMonths, addMonths, isToday } from 'date-fns'
import { ru } from 'date-fns/locale'
import clsx from 'clsx'
&nbsp;
export default function LogHabitModal({ open, onClose, habit, completedDates = [], onLogDate }) {
const [currentMonth, setCurrentMonth] = useState(new Date())
const [selectedDate, setSelectedDate] = useState(null)
const [isLogging, setIsLogging] = useState(false)
&nbsp;
const days = useMemo(() =&gt; {
const start = startOfMonth(currentMonth)
const end = endOfMonth(currentMonth)
return eachDayOfInterval({ start, end })
}, [currentMonth])
&nbsp;
// Convert completedDates to a Set for faster lookup
const completedSet = useMemo(() =&gt; {
const set = new Set()
completedDates.<span class="fstat-no" title="function not covered" >forEach(d</span> =&gt; {
const dateStr = <span class="cstat-no" title="statement not covered" >typeof d === 'string' ? d.split('T')[0] : format(d, 'yyyy-MM-dd')</span>
<span class="cstat-no" title="statement not covered" > set.add(dateStr)</span>
})
return set
}, [completedDates])
&nbsp;
const isDateCompleted = (date) =&gt; {
return completedSet.has(format(date, 'yyyy-MM-dd'))
}
&nbsp;
const <span class="fstat-no" title="function not covered" >handleDateClick = (d</span>ate) =&gt; {
<span class="cstat-no" title="statement not covered" > if (isFuture(startOfDay(date))) <span class="cstat-no" title="statement not covered" >return</span></span>
<span class="cstat-no" title="statement not covered" > if (isDateCompleted(date)) <span class="cstat-no" title="statement not covered" >return</span></span>
<span class="cstat-no" title="statement not covered" > setSelectedDate(date)</span>
}
&nbsp;
const handleConfirm = <span class="fstat-no" title="function not covered" >async () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!selectedDate) <span class="cstat-no" title="statement not covered" >return</span></span>
<span class="cstat-no" title="statement not covered" > setIsLogging(true)</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > await onLogDate(habit.id, format(selectedDate, 'yyyy-MM-dd'))</span>
<span class="cstat-no" title="statement not covered" > onClose()</span>
} catch (error) {
<span class="cstat-no" title="statement not covered" > console.error('Failed to log habit:', error)</span>
} finally {
<span class="cstat-no" title="statement not covered" > setIsLogging(false)</span>
}
}
&nbsp;
// Get first day of week offset
const firstDayOfMonth = startOfMonth(currentMonth)
const startOffset = (firstDayOfMonth.getDay() + 6) % 7 // Monday = 0
&nbsp;
if (!open) return null
&nbsp;
return (
&lt;AnimatePresence&gt;
&lt;motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
&gt;
&lt;motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
onClick={e =&gt; e.stopPropagation()}
className="bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden"
&gt;
{/* Header */}
&lt;div className="p-5 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between"&gt;
&lt;div className="flex items-center gap-3"&gt;
&lt;div
className="w-10 h-10 rounded-xl flex items-center justify-center text-xl"
style={{ backgroundColor: habit?.color + '20' }}
&gt;
{habit?.icon || <span class="branch-1 cbranch-no" title="branch not covered" >'✨'}</span>
&lt;/div&gt;
&lt;div&gt;
&lt;h2 className="text-lg font-display font-bold text-gray-900 dark:text-white"&gt;Отметить привычку&lt;/h2&gt;
&lt;p className="text-sm text-gray-500 dark:text-gray-400 dark:text-gray-500"&gt;{habit?.name}&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;button
onClick={onClose}
className="p-2 text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800 rounded-xl transition-colors"
&gt;
&lt;X size={20} /&gt;
&lt;/button&gt;
&lt;/div&gt;
&nbsp;
{/* Calendar */}
&lt;div className="p-5"&gt;
{/* Month navigation */}
&lt;div className="flex items-center justify-between mb-4"&gt;
&lt;button
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etCurrentMonth(<span class="cstat-no" title="statement not covered" >m</span> =&gt; subMonths(m, 1))</span>}</span>
className="p-2 text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800 rounded-xl transition-colors"
&gt;
&lt;ChevronLeft size={20} /&gt;
&lt;/button&gt;
&lt;span className="font-semibold text-gray-900 dark:text-white capitalize"&gt;
{format(currentMonth, 'LLLL yyyy', { locale: ru })}
&lt;/span&gt;
&lt;button
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etCurrentMonth(<span class="cstat-no" title="statement not covered" >m</span> =&gt; addMonths(m, 1))</span>}</span>
disabled={isSameDay(startOfMonth(currentMonth), startOfMonth(new Date()))}
className={clsx(
"p-2 rounded-xl transition-colors",
isSameDay(startOfMonth(currentMonth), startOfMonth(new Date()))
? "text-gray-200 cursor-not-allowed"
: <span class="branch-1 cbranch-no" title="branch not covered" >"text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800"</span>
)}
&gt;
&lt;ChevronRight size={20} /&gt;
&lt;/button&gt;
&lt;/div&gt;
&nbsp;
{/* Weekday headers */}
&lt;div className="grid grid-cols-7 gap-1 mb-2"&gt;
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day =&gt; (
&lt;div key={day} className="text-center text-xs font-medium text-gray-400 dark:text-gray-500 py-2"&gt;
{day}
&lt;/div&gt;
))}
&lt;/div&gt;
&nbsp;
{/* Calendar grid */}
&lt;div className="grid grid-cols-7 gap-1"&gt;
{/* Empty cells for offset */}
{Array.from({ length: startOffset }).map((_, i) =&gt; (
&lt;div key={`offset-${i}`} className="aspect-square" /&gt;
))}
{/* Days */}
{days.map(day =&gt; {
const completed = isDateCompleted(day)
const future = isFuture(startOfDay(day))
const selected = <span class="branch-1 cbranch-no" title="branch not covered" >selectedDate &amp;&amp; isSameDay(day, selectedDate)</span>
const today = isToday(day)
&nbsp;
return (
&lt;button
key={day.toISOString()}
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >h</span>andleDateClick(day)}</span>
disabled={future || completed}
className={clsx(
"aspect-square rounded-xl flex items-center justify-center text-sm font-medium transition-all",
future &amp;&amp; "text-gray-200 cursor-not-allowed",
completed &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >"bg-green-100 text-green-600 cursor-default",</span>
selected &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >!completed &amp;&amp; <span class="branch-2 cbranch-no" title="branch not covered" >"</span>bg-primary-500 text-white shadow-lg shadow-primary-500/30",</span>
!future &amp;&amp; !completed &amp;&amp; !selected &amp;&amp; "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800",
today &amp;&amp; !selected &amp;&amp; !completed &amp;&amp; "ring-2 ring-primary-200"
)}
&gt;
{completed ? (
<span class="branch-0 cbranch-no" title="branch not covered" > &lt;Check size={16} className="text-green-600" /&gt;</span>
) : (
format(day, 'd')
)}
&lt;/button&gt;
)
})}
&lt;/div&gt;
&nbsp;
{/* Selected date info */}
{selectedDate &amp;&amp; (
<span class="branch-1 cbranch-no" title="branch not covered" > &lt;motion.div</span>
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-3 bg-primary-50 rounded-xl text-center"
&gt;
&lt;p className="text-sm text-primary-700"&gt;
Выбрано: &lt;span className="font-semibold"&gt;{format(selectedDate, 'd MMMM yyyy', { locale: ru })}&lt;/span&gt;
&lt;/p&gt;
&lt;/motion.div&gt;
)}
&lt;/div&gt;
&nbsp;
{/* Actions */}
&lt;div className="p-5 pt-0 flex gap-3"&gt;
&lt;button
onClick={onClose}
className="flex-1 py-3 px-4 rounded-xl font-semibold text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 transition-colors"
&gt;
Отмена
&lt;/button&gt;
&lt;button
onClick={handleConfirm}
disabled={!selectedDate || <span class="branch-1 cbranch-no" title="branch not covered" >isLogging}</span>
className={clsx(
"flex-1 py-3 px-4 rounded-xl font-semibold text-white transition-all",
selectedDate &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >!isLogging</span>
? <span class="branch-0 cbranch-no" title="branch not covered" >"bg-primary-500 hover:bg-primary-600 shadow-lg shadow-primary-500/30"</span>
: "bg-gray-300 cursor-not-allowed"
)}
&gt;
{isLogging ? <span class="branch-0 cbranch-no" title="branch not covered" >'Сохранение...' : '</span>Отметить'}
&lt;/button&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/motion.div&gt;
&lt;/AnimatePresence&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,220 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components/Navigation.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">components</a> Navigation.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>8/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>2/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/4</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>7/7</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { NavLink } from "react-router-dom"
import { Home, BarChart3, PiggyBank, Settings } from "lucide-react"
import { useAuthStore } from "../store/auth"
import clsx from "clsx"
&nbsp;
const OWNER_ID = 1
&nbsp;
export default function Navigation() {
const user = useAuthStore((s) =&gt; s.user)
const isOwner = user?.id === OWNER_ID
&nbsp;
const navItems = [
{ to: "/", icon: Home, label: "Главная" },
{ to: "/tracker", icon: BarChart3, label: "Трекер" },
{ to: "/savings", icon: PiggyBank, label: "Накопления" },
{ to: "/settings", icon: Settings, label: "Настройки" },
].filter(Boolean)
&nbsp;
return (
&lt;nav className="fixed bottom-0 left-0 right-0 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-t border-gray-100 dark:border-gray-800 z-50 transition-colors duration-300"&gt;
&lt;div className="max-w-lg mx-auto px-2"&gt;
&lt;div className="flex items-center justify-around py-2"&gt;
{navItems.map(({ to, icon: Icon, label }) =&gt; (
&lt;NavLink
key={to}
to={to}
end={to === "/"}
className={({ isActive }) =&gt;
clsx(
"flex flex-col items-center gap-0.5 px-2 py-2 rounded-xl transition-all",
isActive
? "text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/30"
: "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
)
}
&gt;
&lt;Icon size={20} /&gt;
&lt;span className="text-[10px] font-medium"&gt;{label}&lt;/span&gt;
&lt;/NavLink&gt;
))}
&lt;/div&gt;
&lt;/div&gt;
&lt;/nav&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,691 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components/finance/FinanceDashboard.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">components/finance</a> FinanceDashboard.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">88.88% </span>
<span class="quiet">Statements</span>
<span class='fraction'>24/27</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">83.33% </span>
<span class="quiet">Branches</span>
<span class='fraction'>15/18</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.57% </span>
<span class="quiet">Functions</span>
<span class='fraction'>11/14</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">86.95% </span>
<span class="quiet">Lines</span>
<span class='fraction'>20/23</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from "react"
import {
PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
} from "recharts"
import { financeApi } from "../../api/finance"
&nbsp;
const COLORS = [
"#0D4F4F", "#F7B538", "#6366f1", "#22c55e", "#ef4444",
"#8b5cf6", "#0ea5e9", "#f97316", "#ec4899", "#14b8a6",
"#64748b", "#a855f7", "#78716c",
]
&nbsp;
const fmt = (n) =&gt; Number(n).toLocaleString("ru-RU") + " ₽"
&nbsp;
export default function FinanceDashboard({ month, year }) {
const [summary, setSummary] = useState(null)
const [loading, setLoading] = useState(true)
&nbsp;
useEffect(() =&gt; {
setLoading(true)
financeApi
.getSummary({ month, year })
.then(setSummary)
.catch(console.error)
.finally(() =&gt; setLoading(false))
}, [month, year])
&nbsp;
if (loading) {
return (
&lt;div className="space-y-4"&gt;
{[1, 2, 3].map((i) =&gt; (
&lt;div key={i} className="card p-6 animate-pulse"&gt;
&lt;div className="h-8 bg-gray-200 dark:bg-gray-800 rounded w-1/2" /&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
)
}
&nbsp;
if (!summary || (summary.total_income === 0 &amp;&amp; summary.total_expense === 0 &amp;&amp; (summary.carried_over || 0) === 0)) {
return (
&lt;div className="card p-12 text-center"&gt;
&lt;span className="text-5xl block mb-4"&gt;📊&lt;/span&gt;
&lt;h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2"&gt;
Нет данных
&lt;/h3&gt;
&lt;p className="text-gray-500 dark:text-gray-400"&gt;
Добавьте первую транзакцию
&lt;/p&gt;
&lt;/div&gt;
)
}
&nbsp;
const expenseCategories = summary.by_category.filter((c) =&gt; c.type === "expense")
const pieData = expenseCategories.map((c) =&gt; ({
name: c.category_emoji + " " + c.category_name,
value: c.amount,
}))
const dailyData = summary.daily.map((d) =&gt; ({
day: d.date.slice(8, 10),
amount: d.amount,
}))
&nbsp;
return (
&lt;div className="space-y-6"&gt;
&lt;div className="card p-6 bg-gradient-to-br from-primary-950 to-primary-800 text-white"&gt;
{summary.carried_over !== 0 &amp;&amp; (
<span class="branch-1 cbranch-no" title="branch not covered" > &lt;p className="text-xs opacity-60 mb-1"&gt;</span>
Остаток с прошлого месяца: &lt;span className={summary.carried_over &gt; 0 ? "text-green-300" : "text-red-300"}&gt;{fmt(summary.carried_over)}&lt;/span&gt;
&lt;/p&gt;
)}
&lt;p className="text-sm opacity-70"&gt;Баланс&lt;/p&gt;
&lt;p className="text-3xl font-bold mt-1"&gt;{fmt(summary.balance)}&lt;/p&gt;
&lt;div className="flex gap-6 mt-4"&gt;
&lt;div&gt;
&lt;p className="text-xs opacity-60"&gt;Доходы&lt;/p&gt;
&lt;p className="text-lg font-semibold text-green-300"&gt;
+{fmt(summary.total_income)}
&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p className="text-xs opacity-60"&gt;Расходы&lt;/p&gt;
&lt;p className="text-lg font-semibold text-red-300"&gt;
-{fmt(summary.total_expense)}
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
{expenseCategories.length &gt; 0 &amp;&amp; (
&lt;div className="card p-5"&gt;
&lt;h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"&gt;
Топ расходов
&lt;/h3&gt;
&lt;div className="space-y-3"&gt;
{expenseCategories.slice(0, 5).map((c, i) =&gt; (
&lt;div key={i} className="flex items-center gap-3"&gt;
&lt;span className="text-xl w-8 text-center"&gt;
{c.category_emoji}
&lt;/span&gt;
&lt;div className="flex-1"&gt;
&lt;div className="flex justify-between mb-1"&gt;
&lt;span className="text-sm font-medium text-gray-700 dark:text-gray-300"&gt;
{c.category_name}
&lt;/span&gt;
&lt;span className="text-sm font-semibold text-gray-900 dark:text-white"&gt;
{fmt(c.amount)}
&lt;/span&gt;
&lt;/div&gt;
&lt;div className="h-2 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden"&gt;
&lt;div
className="h-full rounded-full"
style={{
width: Math.round(c.percentage) + "%",
backgroundColor: COLORS[i % COLORS.length],
}}
/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
&lt;/div&gt;
)}
&nbsp;
{pieData.length &gt; 0 &amp;&amp; (
&lt;div className="card p-5"&gt;
&lt;h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"&gt;
По категориям
&lt;/h3&gt;
&lt;div className="flex items-center gap-4"&gt;
&lt;div className="w-40 h-40"&gt;
&lt;ResponsiveContainer&gt;
&lt;PieChart&gt;
&lt;Pie
data={pieData}
innerRadius={40}
outerRadius={70}
dataKey="value"
stroke="none"
&gt;
{pieData.map((_, i) =&gt; (
&lt;Cell key={i} fill={COLORS[i % COLORS.length]} /&gt;
))}
&lt;/Pie&gt;
&lt;Tooltip <span class="fstat-no" title="function not covered" >formatter={(v</span>) =&gt; <span class="cstat-no" title="statement not covered" >fmt(v)}</span> /&gt;
&lt;/PieChart&gt;
&lt;/ResponsiveContainer&gt;
&lt;/div&gt;
&lt;div className="flex-1 space-y-1"&gt;
{pieData.slice(0, 6).map((c, i) =&gt; (
&lt;div key={i} className="flex items-center gap-2 text-xs"&gt;
&lt;div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: COLORS[i] }}
/&gt;
&lt;span className="text-gray-600 dark:text-gray-400 truncate"&gt;
{c.name}
&lt;/span&gt;
&lt;span className="ml-auto font-medium text-gray-900 dark:text-white"&gt;
{Math.round(
(c.value / summary.total_expense) * 100
)}
%
&lt;/span&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
)}
&nbsp;
{dailyData.length &gt; 0 &amp;&amp; (
&lt;div className="card p-5"&gt;
&lt;h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"&gt;
Расходы по дням
&lt;/h3&gt;
&lt;div className="h-48"&gt;
&lt;ResponsiveContainer&gt;
&lt;LineChart data={dailyData}&gt;
&lt;XAxis dataKey="day" tick={{ fontSize: 12 }} stroke="#94a3b8" /&gt;
&lt;YAxis
tick={{ fontSize: 10 }}
stroke="#94a3b8"
<span class="fstat-no" title="function not covered" > tickFormatter={(v</span>) =&gt; <span class="cstat-no" title="statement not covered" >v / 1000 + "к"}</span>
/&gt;
&lt;Tooltip <span class="fstat-no" title="function not covered" >formatter={(v</span>) =&gt; <span class="cstat-no" title="statement not covered" >fmt(v)}</span> /&gt;
&lt;Line
type="monotone"
dataKey="amount"
stroke="#0D4F4F"
strokeWidth={2}
dot={{ r: 4, fill: "#0D4F4F" }}
/&gt;
&lt;/LineChart&gt;
&lt;/ResponsiveContainer&gt;
&lt;/div&gt;
&lt;/div&gt;
)}
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,586 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components/finance/TransactionList.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">components/finance</a> TransactionList.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">71.42% </span>
<span class="quiet">Statements</span>
<span class='fraction'>35/49</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">61.11% </span>
<span class="quiet">Branches</span>
<span class='fraction'>22/36</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">61.9% </span>
<span class="quiet">Functions</span>
<span class='fraction'>13/21</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">79.06% </span>
<span class="quiet">Lines</span>
<span class='fraction'>34/43</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from "react"
import { financeApi } from "../../api/finance"
&nbsp;
const fmt = (n) =&gt; Number(n).toLocaleString("ru-RU") + " ₽"
&nbsp;
const formatDate = (d) =&gt; {
const dt = new Date(d)
return dt.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })
}
&nbsp;
export default function TransactionList({ onAdd, month, year }) {
const [transactions, setTransactions] = useState([])
const [categories, setCategories] = useState([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState("all")
const [catFilter, setCatFilter] = useState(null)
const [search, setSearch] = useState("")
&nbsp;
useEffect(() =&gt; {
setLoading(true)
Promise.all([
financeApi.listCategories(),
financeApi.listTransactions({
month,
year,
limit: 100,
}),
])
.then(([cats, txs]) =&gt; {
setCategories(cats || <span class="branch-1 cbranch-no" title="branch not covered" >[])</span>
setTransactions(txs || <span class="branch-1 cbranch-no" title="branch not covered" >[])</span>
})
.catch(console.error)
.finally(() =&gt; setLoading(false))
}, [month, year])
&nbsp;
const filtered = transactions.filter((t) =&gt; {
<span class="missing-if-branch" title="if path not taken" >I</span>if (filter !== "all" &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >t.type !== filter) <span class="cstat-no" title="statement not covered" >r</span>eturn false</span>
<span class="missing-if-branch" title="if path not taken" >I</span>if (catFilter &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >t.category_id !== catFilter) <span class="cstat-no" title="statement not covered" >r</span>eturn false</span>
<span class="missing-if-branch" title="if path not taken" >I</span>if (search &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >!t.description.toLowerCase().includes(search.toLowerCase()))</span>
<span class="cstat-no" title="statement not covered" > return false</span>
return true
})
&nbsp;
const grouped = filtered.reduce((acc, t) =&gt; {
const d = t.date.slice(0, 10)
;(acc[d] = acc[d] || []).push(t)
return acc
}, {})
&nbsp;
const handleDelete = <span class="fstat-no" title="function not covered" >async (i</span>d) =&gt; {
<span class="cstat-no" title="statement not covered" > if (!confirm("Удалить транзакцию?")) <span class="cstat-no" title="statement not covered" >return</span></span>
<span class="cstat-no" title="statement not covered" > await financeApi.deleteTransaction(id)</span>
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" > setTransactions((t</span>xs) =&gt; <span class="cstat-no" title="statement not covered" >txs.<span class="fstat-no" title="function not covered" >filter((t</span>) =&gt; <span class="cstat-no" title="statement not covered" >t.id !== id))</span></span></span>
}
&nbsp;
if (loading) {
return (
&lt;div className="space-y-3"&gt;
{[1, 2, 3, 4].map((i) =&gt; (
&lt;div key={i} className="card p-4 animate-pulse"&gt;
&lt;div className="h-5 bg-gray-200 dark:bg-gray-800 rounded w-3/4" /&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
)
}
&nbsp;
return (
&lt;div className="space-y-4"&gt;
&lt;input
className="w-full px-4 py-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white placeholder-gray-400 outline-none"
placeholder="Поиск по описанию..."
value={search}
<span class="fstat-no" title="function not covered" > onChange={(e</span>) =&gt; <span class="cstat-no" title="statement not covered" >setSearch(e.target.value)}</span>
/&gt;
&nbsp;
&lt;div className="flex gap-2"&gt;
{[
["all", "Все"],
["income", "Доходы"],
["expense", "Расходы"],
].map(([k, l]) =&gt; (
&lt;button
key={k}
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etFilter(k)}</span>
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
filter === k
? "bg-primary-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
&gt;
{l}
&lt;/button&gt;
))}
&lt;/div&gt;
&nbsp;
&lt;div className="flex gap-2 overflow-x-auto pb-1"&gt;
&lt;button
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etCatFilter(null)}</span>
className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition ${
!catFilter
? "bg-accent-500 text-white"
: <span class="branch-1 cbranch-no" title="branch not covered" >"bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"</span>
}`}
&gt;
Все
&lt;/button&gt;
{categories.map((c) =&gt; (
&lt;button
key={c.id}
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etCatFilter(c.id)}</span>
className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition ${
catFilter === c.id
? <span class="branch-0 cbranch-no" title="branch not covered" >"bg-accent-500 text-white"</span>
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
&gt;
{c.emoji} {c.name}
&lt;/button&gt;
))}
&lt;/div&gt;
&nbsp;
{Object.keys(grouped).length === 0 ? (
<span class="branch-0 cbranch-no" title="branch not covered" > &lt;div className="card p-12 text-center"&gt;</span>
&lt;span className="text-4xl block mb-3"&gt;🔍&lt;/span&gt;
&lt;p className="text-gray-500 dark:text-gray-400"&gt;Ничего не найдено&lt;/p&gt;
&lt;/div&gt;
) : (
Object.entries(grouped).map(([date, txs]) =&gt; (
&lt;div key={date}&gt;
&lt;p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2"&gt;
{formatDate(date)}
&lt;/p&gt;
&lt;div className="card divide-y divide-gray-100 dark:divide-gray-800"&gt;
{txs.map((t) =&gt; (
&lt;div
key={t.id}
className="px-4 py-3 flex items-center gap-3"
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >h</span>andleDelete(t.id)}</span>
&gt;
&lt;span className="text-xl"&gt;{t.category_emoji}&lt;/span&gt;
&lt;div className="flex-1 min-w-0"&gt;
&lt;p className="text-sm font-medium text-gray-900 dark:text-white truncate"&gt;
{t.description || <span class="branch-1 cbranch-no" title="branch not covered" >t.category_name}</span>
&lt;/p&gt;
&lt;p className="text-xs text-gray-500 dark:text-gray-400"&gt;
{t.category_emoji} {t.category_name}
&lt;/p&gt;
&lt;/div&gt;
&lt;span
className={`text-sm font-bold ${
t.type === "income" ? "text-green-500" : "text-red-500"
}`}
&gt;
{t.type === "income" ? "+" : "-"}
{fmt(t.amount)}
&lt;/span&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
&lt;/div&gt;
))
)}
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,131 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components/finance</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> components/finance</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">77.63% </span>
<span class="quiet">Statements</span>
<span class='fraction'>59/76</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">68.51% </span>
<span class="quiet">Branches</span>
<span class='fraction'>37/54</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">68.57% </span>
<span class="quiet">Functions</span>
<span class='fraction'>24/35</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">81.81% </span>
<span class="quiet">Lines</span>
<span class='fraction'>54/66</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="FinanceDashboard.jsx"><a href="FinanceDashboard.jsx.html">FinanceDashboard.jsx</a></td>
<td data-value="88.88" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 88%"></div><div class="cover-empty" style="width: 12%"></div></div>
</td>
<td data-value="88.88" class="pct high">88.88%</td>
<td data-value="27" class="abs high">24/27</td>
<td data-value="83.33" class="pct high">83.33%</td>
<td data-value="18" class="abs high">15/18</td>
<td data-value="78.57" class="pct medium">78.57%</td>
<td data-value="14" class="abs medium">11/14</td>
<td data-value="86.95" class="pct high">86.95%</td>
<td data-value="23" class="abs high">20/23</td>
</tr>
<tr>
<td class="file medium" data-value="TransactionList.jsx"><a href="TransactionList.jsx.html">TransactionList.jsx</a></td>
<td data-value="71.42" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 71%"></div><div class="cover-empty" style="width: 29%"></div></div>
</td>
<td data-value="71.42" class="pct medium">71.42%</td>
<td data-value="49" class="abs medium">35/49</td>
<td data-value="61.11" class="pct medium">61.11%</td>
<td data-value="36" class="abs medium">22/36</td>
<td data-value="61.9" class="pct medium">61.9%</td>
<td data-value="21" class="abs medium">13/21</td>
<td data-value="79.06" class="pct medium">79.06%</td>
<td data-value="43" class="abs medium">34/43</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,191 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> components</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">62.77% </span>
<span class="quiet">Statements</span>
<span class='fraction'>285/454</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">49.74% </span>
<span class="quiet">Branches</span>
<span class='fraction'>195/392</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">33.33% </span>
<span class="quiet">Functions</span>
<span class='fraction'>55/165</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">63.88% </span>
<span class="quiet">Lines</span>
<span class='fraction'>283/443</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file medium" data-value="CreateHabitModal.jsx"><a href="CreateHabitModal.jsx.html">CreateHabitModal.jsx</a></td>
<td data-value="65.82" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 65%"></div><div class="cover-empty" style="width: 35%"></div></div>
</td>
<td data-value="65.82" class="pct medium">65.82%</td>
<td data-value="79" class="abs medium">52/79</td>
<td data-value="49.15" class="pct low">49.15%</td>
<td data-value="59" class="abs low">29/59</td>
<td data-value="28.57" class="pct low">28.57%</td>
<td data-value="28" class="abs low">8/28</td>
<td data-value="65.82" class="pct medium">65.82%</td>
<td data-value="79" class="abs medium">52/79</td>
</tr>
<tr>
<td class="file medium" data-value="CreateTaskModal.jsx"><a href="CreateTaskModal.jsx.html">CreateTaskModal.jsx</a></td>
<td data-value="71.05" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 71%"></div><div class="cover-empty" style="width: 29%"></div></div>
</td>
<td data-value="71.05" class="pct medium">71.05%</td>
<td data-value="76" class="abs medium">54/76</td>
<td data-value="54.09" class="pct medium">54.09%</td>
<td data-value="61" class="abs medium">33/61</td>
<td data-value="32.14" class="pct low">32.14%</td>
<td data-value="28" class="abs low">9/28</td>
<td data-value="71.05" class="pct medium">71.05%</td>
<td data-value="76" class="abs medium">54/76</td>
</tr>
<tr>
<td class="file medium" data-value="EditHabitModal.jsx"><a href="EditHabitModal.jsx.html">EditHabitModal.jsx</a></td>
<td data-value="52.94" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 52%"></div><div class="cover-empty" style="width: 48%"></div></div>
</td>
<td data-value="52.94" class="pct medium">52.94%</td>
<td data-value="153" class="abs medium">81/153</td>
<td data-value="39.23" class="pct low">39.23%</td>
<td data-value="130" class="abs low">51/130</td>
<td data-value="24.07" class="pct low">24.07%</td>
<td data-value="54" class="abs low">13/54</td>
<td data-value="54" class="pct medium">54%</td>
<td data-value="150" class="abs medium">81/150</td>
</tr>
<tr>
<td class="file medium" data-value="EditTaskModal.jsx"><a href="EditTaskModal.jsx.html">EditTaskModal.jsx</a></td>
<td data-value="68.88" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 68%"></div><div class="cover-empty" style="width: 32%"></div></div>
</td>
<td data-value="68.88" class="pct medium">68.88%</td>
<td data-value="90" class="abs medium">62/90</td>
<td data-value="57.44" class="pct medium">57.44%</td>
<td data-value="94" class="abs medium">54/94</td>
<td data-value="37.14" class="pct low">37.14%</td>
<td data-value="35" class="abs low">13/35</td>
<td data-value="69.66" class="pct medium">69.66%</td>
<td data-value="89" class="abs medium">62/89</td>
</tr>
<tr>
<td class="file medium" data-value="LogHabitModal.jsx"><a href="LogHabitModal.jsx.html">LogHabitModal.jsx</a></td>
<td data-value="58.33" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 58%"></div><div class="cover-empty" style="width: 42%"></div></div>
</td>
<td data-value="58.33" class="pct medium">58.33%</td>
<td data-value="48" class="abs medium">28/48</td>
<td data-value="56.52" class="pct medium">56.52%</td>
<td data-value="46" class="abs medium">26/46</td>
<td data-value="50" class="pct medium">50%</td>
<td data-value="16" class="abs medium">8/16</td>
<td data-value="64.28" class="pct medium">64.28%</td>
<td data-value="42" class="abs medium">27/42</td>
</tr>
<tr>
<td class="file high" data-value="Navigation.jsx"><a href="Navigation.jsx.html">Navigation.jsx</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="4" class="abs high">4/4</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="7" class="abs high">7/7</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,211 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for contexts/ThemeContext.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">contexts</a> ThemeContext.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">94.73% </span>
<span class="quiet">Statements</span>
<span class='fraction'>18/19</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90% </span>
<span class="quiet">Branches</span>
<span class='fraction'>9/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>6/6</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">94.44% </span>
<span class="quiet">Lines</span>
<span class='fraction'>17/18</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">46x</span>
<span class="cline-any cline-yes">46x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">41x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { createContext, useContext, useEffect, useState } from "react"
&nbsp;
const ThemeContext = createContext()
&nbsp;
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() =&gt; {
<span class="missing-if-branch" title="else path not taken" >E</span>if (typeof window !== "undefined") {
return localStorage.getItem("theme") || "dark"
}
<span class="cstat-no" title="statement not covered" > return "dark"</span>
})
&nbsp;
useEffect(() =&gt; {
const root = window.document.documentElement
if (theme === "dark") {
root.classList.add("dark")
} else {
root.classList.remove("dark")
}
localStorage.setItem("theme", theme)
}, [theme])
&nbsp;
const toggleTheme = () =&gt; {
setTheme(prev =&gt; prev === "dark" ? "light" : "dark")
}
&nbsp;
return (
&lt;ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}&gt;
{children}
&lt;/ThemeContext.Provider&gt;
)
}
&nbsp;
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error("useTheme must be used within ThemeProvider")
}
return context
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,116 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for contexts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> contexts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">94.73% </span>
<span class="quiet">Statements</span>
<span class='fraction'>18/19</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90% </span>
<span class="quiet">Branches</span>
<span class='fraction'>9/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>6/6</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">94.44% </span>
<span class="quiet">Lines</span>
<span class='fraction'>17/18</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="ThemeContext.jsx"><a href="ThemeContext.jsx.html">ThemeContext.jsx</a></td>
<td data-value="94.73" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 94%"></div><div class="cover-empty" style="width: 6%"></div></div>
</td>
<td data-value="94.73" class="pct high">94.73%</td>
<td data-value="19" class="abs high">18/19</td>
<td data-value="90" class="pct high">90%</td>
<td data-value="10" class="abs high">9/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="6" class="abs high">6/6</td>
<td data-value="94.44" class="pct high">94.44%</td>
<td data-value="18" class="abs high">17/18</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 B

View File

@@ -1,191 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">56.61% </span>
<span class="quiet">Statements</span>
<span class='fraction'>929/1641</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">45.66% </span>
<span class="quiet">Branches</span>
<span class='fraction'>580/1270</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">42.06% </span>
<span class="quiet">Functions</span>
<span class='fraction'>257/611</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">59.7% </span>
<span class="quiet">Lines</span>
<span class='fraction'>877/1469</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="api"><a href="api/index.html">api</a></td>
<td data-value="89.1" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 89%"></div><div class="cover-empty" style="width: 11%"></div></div>
</td>
<td data-value="89.1" class="pct high">89.1%</td>
<td data-value="101" class="abs high">90/101</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="12" class="abs high">12/12</td>
<td data-value="84.93" class="pct high">84.93%</td>
<td data-value="73" class="abs high">62/73</td>
<td data-value="90" class="pct high">90%</td>
<td data-value="80" class="abs high">72/80</td>
</tr>
<tr>
<td class="file medium" data-value="components"><a href="components/index.html">components</a></td>
<td data-value="62.77" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 62%"></div><div class="cover-empty" style="width: 38%"></div></div>
</td>
<td data-value="62.77" class="pct medium">62.77%</td>
<td data-value="454" class="abs medium">285/454</td>
<td data-value="49.74" class="pct low">49.74%</td>
<td data-value="392" class="abs low">195/392</td>
<td data-value="33.33" class="pct low">33.33%</td>
<td data-value="165" class="abs low">55/165</td>
<td data-value="63.88" class="pct medium">63.88%</td>
<td data-value="443" class="abs medium">283/443</td>
</tr>
<tr>
<td class="file medium" data-value="components/finance"><a href="components/finance/index.html">components/finance</a></td>
<td data-value="77.63" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 77%"></div><div class="cover-empty" style="width: 23%"></div></div>
</td>
<td data-value="77.63" class="pct medium">77.63%</td>
<td data-value="76" class="abs medium">59/76</td>
<td data-value="68.51" class="pct medium">68.51%</td>
<td data-value="54" class="abs medium">37/54</td>
<td data-value="68.57" class="pct medium">68.57%</td>
<td data-value="35" class="abs medium">24/35</td>
<td data-value="81.81" class="pct high">81.81%</td>
<td data-value="66" class="abs high">54/66</td>
</tr>
<tr>
<td class="file high" data-value="contexts"><a href="contexts/index.html">contexts</a></td>
<td data-value="94.73" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 94%"></div><div class="cover-empty" style="width: 6%"></div></div>
</td>
<td data-value="94.73" class="pct high">94.73%</td>
<td data-value="19" class="abs high">18/19</td>
<td data-value="90" class="pct high">90%</td>
<td data-value="10" class="abs high">9/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="6" class="abs high">6/6</td>
<td data-value="94.44" class="pct high">94.44%</td>
<td data-value="18" class="abs high">17/18</td>
</tr>
<tr>
<td class="file low" data-value="pages"><a href="pages/index.html">pages</a></td>
<td data-value="46.79" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 46%"></div><div class="cover-empty" style="width: 54%"></div></div>
</td>
<td data-value="46.79" class="pct low">46.79%</td>
<td data-value="966" class="abs low">452/966</td>
<td data-value="40.62" class="pct low">40.62%</td>
<td data-value="800" class="abs low">325/800</td>
<td data-value="32.11" class="pct low">32.11%</td>
<td data-value="327" class="abs low">105/327</td>
<td data-value="50.95" class="pct medium">50.95%</td>
<td data-value="838" class="abs medium">427/838</td>
</tr>
<tr>
<td class="file high" data-value="store"><a href="store/index.html">store</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="25" class="abs high">25/25</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="24" class="abs high">24/24</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View File

@@ -1,451 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/Finance.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Finance.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">44.44% </span>
<span class="quiet">Statements</span>
<span class='fraction'>16/36</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">75% </span>
<span class="quiet">Branches</span>
<span class='fraction'>15/20</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">25% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/16</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">64% </span>
<span class="quiet">Lines</span>
<span class='fraction'>16/25</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">56x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from "react"
import Navigation from "../components/Navigation"
import FinanceDashboard from "../components/finance/FinanceDashboard"
import TransactionList from "../components/finance/TransactionList"
import FinanceAnalytics from "../components/finance/FinanceAnalytics"
import CategoriesManager from "../components/finance/CategoriesManager"
import AddTransactionModal from "../components/finance/AddTransactionModal"
&nbsp;
const tabs = [
{ key: "dashboard", label: "Обзор", icon: "📊" },
{ key: "transactions", label: "Транзакции", icon: "📋" },
{ key: "analytics", label: "Аналитика", icon: "📈" },
{ key: "categories", label: "Категории", icon: "🏷️" },
]
&nbsp;
const MONTH_NAMES = [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь",
]
&nbsp;
export default function Finance() {
const now = new Date()
const [activeTab, setActiveTab] = useState("dashboard")
const [showAdd, setShowAdd] = useState(false)
const [refreshKey, setRefreshKey] = useState(0)
const [month, setMonth] = useState(now.getMonth() + 1)
const [year, setYear] = useState(now.getFullYear())
&nbsp;
const <span class="fstat-no" title="function not covered" >refresh = () =&gt; <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etRefreshKey((k</span>) =&gt; <span class="cstat-no" title="statement not covered" >k + 1)</span></span>
&nbsp;
const <span class="fstat-no" title="function not covered" >prevMonth = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (month === 1) { <span class="cstat-no" title="statement not covered" >setMonth(12); <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etYear(y</span> =&gt; <span class="cstat-no" title="statement not covered" >y - 1) </span>}</span></span>
else <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >setMonth(m</span> =&gt; <span class="cstat-no" title="statement not covered" >m - 1)</span></span>
}
const <span class="fstat-no" title="function not covered" >nextMonth = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (month === 12) { <span class="cstat-no" title="statement not covered" >setMonth(1); <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etYear(y</span> =&gt; <span class="cstat-no" title="statement not covered" >y + 1) </span>}</span></span>
else <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >setMonth(m</span> =&gt; <span class="cstat-no" title="statement not covered" >m + 1)</span></span>
}
&nbsp;
const isCurrentMonth = month === now.getMonth() + 1 &amp;&amp; year === now.getFullYear()
&nbsp;
return (
&lt;div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24"&gt;
&lt;header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"&gt;
&lt;div className="max-w-lg mx-auto px-4 py-4 flex items-center justify-between"&gt;
&lt;div&gt;
&lt;h1 className="text-xl font-display font-bold text-gray-900 dark:text-white"&gt;
💰 Финансы
&lt;/h1&gt;
&lt;/div&gt;
&lt;button
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowAdd(true)}</span>
className="w-10 h-10 rounded-xl bg-primary-500 text-white flex items-center justify-center text-xl shadow-lg hover:bg-primary-600 transition"
&gt;
+
&lt;/button&gt;
&lt;/div&gt;
&nbsp;
{/* Month Switcher */}
&lt;div className="max-w-lg mx-auto px-4 pb-3"&gt;
&lt;div className="flex items-center justify-center gap-4"&gt;
&lt;button
onClick={prevMonth}
className="w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition text-sm font-bold"
&gt;
&lt;/button&gt;
&lt;button
<span class="fstat-no" title="function not covered" > onClick={() =&gt; {</span> <span class="cstat-no" title="statement not covered" >setMonth(now.getMonth() + 1); <span class="cstat-no" title="statement not covered" >s</span>etYear(now.getFullYear()) }}</span>
className={"text-sm font-semibold min-w-[140px] text-center " + (isCurrentMonth ? "text-gray-900 dark:text-white" : <span class="branch-1 cbranch-no" title="branch not covered" >"text-primary-600 dark:text-primary-400")}</span>
&gt;
{MONTH_NAMES[month - 1]} {year}
&lt;/button&gt;
&lt;button
onClick={nextMonth}
className="w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition text-sm font-bold"
&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="max-w-lg mx-auto px-4 pb-3 flex gap-1.5 overflow-x-auto scrollbar-hide"&gt;
{tabs.map((t) =&gt; (
&lt;button
key={t.key}
onClick={() =&gt; setActiveTab(t.key)}
className={`flex-1 min-w-0 py-2 rounded-xl text-xs sm:text-sm font-semibold transition whitespace-nowrap px-2 ${
activeTab === t.key
? "bg-primary-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
&gt;
{t.icon} {t.label}
&lt;/button&gt;
))}
&lt;/div&gt;
&lt;/header&gt;
&nbsp;
&lt;div className="max-w-lg mx-auto px-4 py-6"&gt;
{activeTab === "dashboard" &amp;&amp; &lt;FinanceDashboard key={refreshKey + "-" + month + "-" + year} month={month} year={year} /&gt;}
{activeTab === "transactions" &amp;&amp; (
&lt;TransactionList key={refreshKey + "-" + month + "-" + year} month={month} year={year} onAdd={() =&gt; setShowAdd(true)} /&gt;
)}
{activeTab === "analytics" &amp;&amp; &lt;FinanceAnalytics key={refreshKey + "-" + month + "-" + year} month={month} year={year} /&gt;}
{activeTab === "categories" &amp;&amp; &lt;CategoriesManager refreshKey={refreshKey} /&gt;}
&lt;/div&gt;
&nbsp;
{showAdd &amp;&amp; (
&lt;AddTransactionModal
<span class="fstat-no" title="function not covered" > onClose={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowAdd(false)}</span>
<span class="fstat-no" title="function not covered" > onSaved={() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > setShowAdd(false)</span>
<span class="cstat-no" title="statement not covered" > refresh()</span>
}}
/&gt;
)}
&nbsp;
&lt;Navigation /&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,499 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/ForgotPassword.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> ForgotPassword.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>17/17</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>8/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>17/17</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
import { Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Mail, ArrowLeft, Zap, CheckCircle } from 'lucide-react'
import api from '../api/client'
&nbsp;
export default function ForgotPassword() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [sent, setSent] = useState(false)
&nbsp;
const handleSubmit = async (e) =&gt; {
e.preventDefault()
setError('')
setLoading(true)
&nbsp;
try {
await api.post('/auth/forgot-password', { email })
setSent(true)
} catch (err) {
setError(err.response?.data?.error || 'Ошибка отправки')
} finally {
setLoading(false)
}
}
&nbsp;
if (sent) {
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50"&gt;
&lt;motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md"
&gt;
&lt;div className="card p-10 text-center"&gt;
&lt;motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }}
className="w-20 h-20 rounded-3xl bg-green-100 flex items-center justify-center mx-auto mb-6"
&gt;
&lt;CheckCircle className="w-10 h-10 text-green-600" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-2xl font-display font-bold text-gray-900 mb-2"&gt;
Письмо отправлено! 📬
&lt;/h1&gt;
&lt;p className="text-gray-500 mb-6"&gt;
Если аккаунт с email &lt;strong&gt;{email}&lt;/strong&gt; существует, мы отправили ссылку для сброса пароля.
&lt;/p&gt;
&lt;Link to="/login" className="btn btn-primary"&gt;
Вернуться ко входу
&lt;/Link&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50"&gt;
&lt;motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md"
&gt;
&lt;div className="text-center mb-8"&gt;
&lt;motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: 0.1 }}
className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-500 to-primary-700 mb-6 shadow-xl shadow-primary-500/30"
&gt;
&lt;Mail className="w-10 h-10 text-white" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-3xl font-display font-bold text-gray-900"&gt;
Забыли пароль?
&lt;/h1&gt;
&lt;p className="text-gray-500 mt-2"&gt;
Введи email и мы отправим ссылку для сброса
&lt;/p&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="card p-8"&gt;
&lt;form onSubmit={handleSubmit} className="space-y-5"&gt;
{error &amp;&amp; (
&lt;motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="p-4 rounded-2xl bg-red-50 text-red-600 text-sm font-medium"
&gt;
{error}
&lt;/motion.div&gt;
)}
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-semibold text-gray-700 mb-2"&gt;
Email
&lt;/label&gt;
&lt;input
type="email"
value={email}
onChange={(e) =&gt; setEmail(e.target.value)}
className="input"
placeholder="your@email.com"
required
autoFocus
/&gt;
&lt;/div&gt;
&nbsp;
&lt;button
type="submit"
disabled={loading}
className="btn btn-primary w-full text-lg"
&gt;
{loading ? 'Отправляем...' : 'Отправить ссылку'}
&lt;/button&gt;
&lt;/form&gt;
&nbsp;
&lt;div className="mt-6 text-center"&gt;
&lt;Link
to="/login"
className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium text-sm"
&gt;
&lt;ArrowLeft size={16} /&gt;
Вернуться ко входу
&lt;/Link&gt;
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="flex items-center justify-center gap-2 mt-6 text-gray-400"&gt;
&lt;Zap size={16} /&gt;
&lt;span className="text-sm font-medium"&gt;Pulse&lt;/span&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,745 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/Habits.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Habits.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">64.91% </span>
<span class="quiet">Statements</span>
<span class='fraction'>37/57</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">65.9% </span>
<span class="quiet">Branches</span>
<span class='fraction'>29/44</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">53.57% </span>
<span class="quiet">Functions</span>
<span class='fraction'>15/28</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">65.21% </span>
<span class="quiet">Lines</span>
<span class='fraction'>30/46</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import { Plus, Settings, Flame, Calendar, ChevronRight, Archive, ArchiveRestore } from 'lucide-react'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
import { habitsApi } from '../api/habits'
import CreateHabitModal from '../components/CreateHabitModal'
import EditHabitModal from '../components/EditHabitModal'
import Navigation from '../components/Navigation'
import clsx from 'clsx'
&nbsp;
export default function Habits({ embedded = false }) {
const [showCreateModal, setShowCreateModal] = useState(false)
const [editingHabit, setEditingHabit] = useState(null)
const [showArchived, setShowArchived] = useState(false)
const [habitStats, setHabitStats] = useState({})
const queryClient = useQueryClient()
&nbsp;
const { data: habits = [], isLoading } = useQuery({
queryKey: ['habits', showArchived],
queryFn: () =&gt; habitsApi.list().then(h =&gt; showArchived ? <span class="branch-0 cbranch-no" title="branch not covered" >h : h</span>.filter(x =&gt; !x.is_archived)),
})
&nbsp;
const { data: archivedHabits = [] } = useQuery({
queryKey: ['habits-archived'],
<span class="fstat-no" title="function not covered" > queryFn: () =&gt; <span class="cstat-no" title="statement not covered" >h</span>abitsApi.list().<span class="fstat-no" title="function not covered" >then(h</span> =&gt; <span class="cstat-no" title="statement not covered" >h.<span class="fstat-no" title="function not covered" >filter(x</span> =&gt; <span class="cstat-no" title="statement not covered" >x.is_archived))</span></span>,</span>
enabled: showArchived,
})
&nbsp;
useEffect(() =&gt; {
if (habits.length &gt; 0) loadStats()
}, [habits])
&nbsp;
const loadStats = async () =&gt; {
const statsMap = {}
await Promise.all(habits.map(async (habit) =&gt; {
try {
const stats = await habitsApi.getHabitStats(habit.id)
statsMap[habit.id] = stats
} catch (e) {}
}))
setHabitStats(statsMap)
}
&nbsp;
const archiveMutation = useMutation({
<span class="fstat-no" title="function not covered" > mutationFn: ({</span> id, archived }) =&gt; <span class="cstat-no" title="statement not covered" >habitsApi.update(id, { is_archived: archived }),</span>
<span class="fstat-no" title="function not covered" > onSuccess: () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['habits'] })</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['habits-archived'] })</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['stats'] })</span>
},
})
&nbsp;
const getFrequencyLabel = (habit) =&gt; {
if (habit.frequency === 'daily') return 'Ежедневно'
<span class="missing-if-branch" title="else path not taken" >E</span>if (habit.frequency === 'weekly' &amp;&amp; habit.target_days) {
const days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
return habit.target_days.map(d =&gt; days[d - 1]).join(', ')
}
<span class="cstat-no" title="statement not covered" > if (habit.frequency === 'interval') <span class="cstat-no" title="statement not covered" >return `Каждые ${habit.target_count} дн.`</span></span>
<span class="cstat-no" title="statement not covered" > if (habit.frequency === 'custom') <span class="cstat-no" title="statement not covered" >return `Каждые ${habit.target_count} дн.`</span></span>
<span class="cstat-no" title="statement not covered" > return habit.frequency</span>
}
&nbsp;
const activeHabits = habits.filter(h =&gt; !h.is_archived)
const archivedList = habits.filter(h =&gt; h.is_archived)
&nbsp;
return (
&lt;div className={embedded ? "" : "min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"}&gt;
{!embedded &amp;&amp; &lt;header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"&gt;
&lt;div className="max-w-lg mx-auto px-4 py-4 flex items-center justify-between"&gt;
&lt;div&gt;
&lt;h1 className="text-xl font-display font-bold text-gray-900 dark:text-white"&gt;Мои привычки&lt;/h1&gt;
&lt;p className="text-sm text-gray-500 dark:text-gray-400"&gt;{activeHabits.length} активных&lt;/p&gt;
&lt;/div&gt;
&lt;button onClick={() =&gt; setShowCreateModal(true)} className="btn btn-primary flex items-center gap-2"&gt;
&lt;Plus size={18} /&gt;
Новая
&lt;/button&gt;
&lt;/div&gt;
&lt;/header&gt;}
&nbsp;
&lt;main className="max-w-lg mx-auto px-4 py-6 space-y-6"&gt;
{isLoading ? (
&lt;div className="space-y-4"&gt;
{[1, 2, 3].map((i) =&gt; (
&lt;div key={i} className="card p-5 animate-pulse"&gt;
&lt;div className="flex items-center gap-4"&gt;
&lt;div className="w-14 h-14 rounded-2xl bg-gray-200 dark:bg-gray-700" /&gt;
&lt;div className="flex-1"&gt;
&lt;div className="h-5 bg-gray-200 dark:bg-gray-700 rounded-lg w-1/2 mb-2" /&gt;
&lt;div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-lg w-1/3" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
) : activeHabits.length === 0 &amp;&amp; !showArchived ? (
&lt;motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="card p-10 text-center"&gt;
&lt;div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-100 to-accent-100 dark:from-primary-900/30 dark:to-accent-900/30 flex items-center justify-center mx-auto mb-5"&gt;
&lt;Plus className="w-10 h-10 text-primary-600 dark:text-primary-400" /&gt;
&lt;/div&gt;
&lt;h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2"&gt;Нет привычек&lt;/h3&gt;
&lt;p className="text-gray-500 dark:text-gray-400 mb-6"&gt;Создай свою первую привычку!&lt;/p&gt;
&lt;button <span class="fstat-no" title="function not covered" >onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowCreateModal(true)}</span> className="btn btn-primary"&gt;
&lt;Plus size={20} className="mr-2" /&gt;
Создать привычку
&lt;/button&gt;
&lt;/motion.div&gt;
) : (
&lt;&gt;
&lt;div className="space-y-3"&gt;
&lt;AnimatePresence&gt;
{activeHabits.map((habit, index) =&gt; (
&lt;HabitListItem
key={habit.id}
habit={habit}
index={index}
stats={habitStats[habit.id]}
frequencyLabel={getFrequencyLabel(habit)}
<span class="fstat-no" title="function not covered" > onEdit={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etEditingHabit(habit)}</span>
<span class="fstat-no" title="function not covered" > onArchive={() =&gt; <span class="cstat-no" title="statement not covered" >a</span>rchiveMutation.mutate({ id: habit.id, archived: true })}</span>
/&gt;
))}
&lt;/AnimatePresence&gt;
&lt;/div&gt;
&nbsp;
{archivedList.length &gt; 0 &amp;&amp; (
<span class="branch-1 cbranch-no" title="branch not covered" > &lt;div className="mt-8"&gt;</span>
&lt;button <span class="fstat-no" title="function not covered" >onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowArchived(!showArchived)}</span> className="flex items-center gap-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 mb-4"&gt;
&lt;Archive size={18} /&gt;
&lt;span className="font-medium"&gt;Архив ({archivedList.length})&lt;/span&gt;
&lt;ChevronRight size={18} className={clsx('transition-transform', showArchived &amp;&amp; 'rotate-90')} /&gt;
&lt;/button&gt;
&nbsp;
&lt;AnimatePresence&gt;
{showArchived &amp;&amp; (
&lt;motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} className="space-y-3"&gt;
{archivedList.<span class="fstat-no" title="function not covered" >map((h</span>abit, index) =&gt; (
<span class="cstat-no" title="statement not covered" > &lt;motion.div</span>
key={habit.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="card p-4 opacity-60"
&gt;
&lt;div className="flex items-center gap-4"&gt;
&lt;div className="w-12 h-12 rounded-xl flex items-center justify-center text-xl" style={{ backgroundColor: habit.color + '20' }}&gt;
{habit.icon || '✨'}
&lt;/div&gt;
&lt;div className="flex-1 min-w-0"&gt;
&lt;h3 className="font-semibold text-gray-600 dark:text-gray-400 truncate"&gt;{habit.name}&lt;/h3&gt;
&lt;p className="text-sm text-gray-400 dark:text-gray-500"&gt;{getFrequencyLabel(habit)}&lt;/p&gt;
&lt;/div&gt;
&lt;button <span class="fstat-no" title="function not covered" >onClick={() =&gt; <span class="cstat-no" title="statement not covered" >a</span>rchiveMutation.mutate({ id: habit.id, archived: false })}</span> className="p-2 text-gray-400 hover:text-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 rounded-xl transition-all" title="Восстановить"&gt;
&lt;ArchiveRestore size={20} /&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
))}
&lt;/motion.div&gt;
)}
&lt;/AnimatePresence&gt;
&lt;/div&gt;
)}
&lt;/&gt;
)}
&lt;/main&gt;
&nbsp;
{!embedded &amp;&amp; &lt;Navigation /&gt;}
&lt;CreateHabitModal open={showCreateModal} <span class="fstat-no" title="function not covered" >onClose={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowCreateModal(false)} /&gt;</span>
&lt;EditHabitModal open={!!editingHabit} <span class="fstat-no" title="function not covered" >onClose={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etEditingHabit(null)}</span> habit={editingHabit} /&gt;
&lt;/div&gt;
)
}
&nbsp;
function HabitListItem({ habit, index, stats, frequencyLabel, onEdit, onArchive }) {
return (
&lt;motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ delay: index * 0.05 }}
onClick={onEdit}
className="card p-4 cursor-pointer hover:shadow-lg transition-all"
&gt;
&lt;div className="flex items-center gap-4"&gt;
&lt;div className="w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0" style={{ backgroundColor: habit.color + '15' }}&gt;
{habit.icon || <span class="branch-1 cbranch-no" title="branch not covered" >'✨'}</span>
&lt;/div&gt;
&lt;div className="flex-1 min-w-0"&gt;
&lt;h3 className="font-semibold text-gray-900 dark:text-white truncate"&gt;{habit.name}&lt;/h3&gt;
&lt;div className="flex items-center gap-3 mt-1"&gt;
&lt;span className="text-xs font-medium px-2 py-0.5 rounded-full" style={{ backgroundColor: habit.color + '15', color: habit.color }}&gt;
{frequencyLabel}
&lt;/span&gt;
{stats &amp;&amp; stats.current_streak &gt; 0 &amp;&amp; (
<span class="branch-2 cbranch-no" title="branch not covered" > &lt;span className="text-xs text-orange-500 flex items-center gap-1"&gt;</span>
&lt;Flame size={14} /&gt;
{stats.current_streak} дн.
&lt;/span&gt;
)}
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="flex items-center gap-2"&gt;
{stats &amp;&amp; (
&lt;div className="text-right"&gt;
&lt;p className="text-sm font-semibold text-gray-900 dark:text-white"&gt;{stats.this_month}&lt;/p&gt;
&lt;p className="text-xs text-gray-400 dark:text-gray-500"&gt;в месяц&lt;/p&gt;
&lt;/div&gt;
)}
&lt;ChevronRight size={20} className="text-gray-300 dark:text-gray-600" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,373 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/Login.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Login.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>21/21</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>10/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>6/6</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>20/20</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Eye, EyeOff, Zap } from 'lucide-react'
import { useAuthStore } from '../store/auth'
&nbsp;
export default function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const login = useAuthStore(s =&gt; s.login)
const navigate = useNavigate()
&nbsp;
const handleSubmit = async (e) =&gt; {
e.preventDefault()
setError('')
setLoading(true)
&nbsp;
try {
await login(email, password)
navigate('/')
} catch (err) {
setError(err.response?.data?.error || 'Ошибка входа')
} finally {
setLoading(false)
}
}
&nbsp;
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50 dark:bg-gray-950 transition-colors duration-300"&gt;
&lt;motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md"&gt;
&lt;div className="text-center mb-8"&gt;
&lt;motion.div initial={{ scale: 0, rotate: -180 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: 'spring', delay: 0.1, stiffness: 200 }} className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-500 to-primary-700 mb-6 shadow-xl shadow-primary-500/30"&gt;
&lt;Zap className="w-10 h-10 text-white" /&gt;
&lt;/motion.div&gt;
&lt;motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.2 }} className="text-3xl font-display font-bold text-gray-900 dark:text-white"&gt;
С возвращением!
&lt;/motion.h1&gt;
&lt;motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }} className="text-gray-500 dark:text-gray-400 mt-2"&gt;
Войди, чтобы продолжить
&lt;/motion.p&gt;
&lt;/div&gt;
&nbsp;
&lt;motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="card p-8"&gt;
&lt;form onSubmit={handleSubmit} className="space-y-5"&gt;
{error &amp;&amp; (
&lt;motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="p-4 rounded-2xl bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm font-medium"&gt;
{error}
&lt;/motion.div&gt;
)}
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"&gt;Email&lt;/label&gt;
&lt;input type="email" value={email} onChange={(e) =&gt; setEmail(e.target.value)} className="input" placeholder="your@email.com" required /&gt;
&lt;/div&gt;
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"&gt;Пароль&lt;/label&gt;
&lt;div className="relative"&gt;
&lt;input type={showPassword ? 'text' : 'password'} value={password} onChange={(e) =&gt; setPassword(e.target.value)} className="input pr-12" placeholder="••••••••" required /&gt;
&lt;button type="button" onClick={() =&gt; setShowPassword(!showPassword)} className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"&gt;
{showPassword ? &lt;EyeOff size={20} /&gt; : &lt;Eye size={20} /&gt;}
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="flex justify-end"&gt;
&lt;Link to="/forgot-password" className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 font-medium"&gt;Забыли пароль?&lt;/Link&gt;
&lt;/div&gt;
&nbsp;
&lt;button type="submit" disabled={loading} className="btn btn-primary w-full text-lg"&gt;
{loading ? (
&lt;span className="flex items-center gap-2"&gt;
&lt;svg className="animate-spin h-5 w-5" viewBox="0 0 24 24"&gt;
&lt;circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" /&gt;
&lt;path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /&gt;
&lt;/svg&gt;
Входим...
&lt;/span&gt;
) : 'Войти'}
&lt;/button&gt;
&lt;/form&gt;
&nbsp;
&lt;div className="mt-8 pt-6 border-t border-gray-100 dark:border-gray-800 text-center"&gt;
&lt;p className="text-gray-500 dark:text-gray-400"&gt;
Нет аккаунта?{' '}&lt;Link to="/register" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 font-semibold"&gt;Зарегистрируйся&lt;/Link&gt;
&lt;/p&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,337 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/Register.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Register.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>23/23</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>10/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>7/7</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>22/22</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Eye, EyeOff, Sparkles } from 'lucide-react'
import { useAuthStore } from '../store/auth'
&nbsp;
export default function Register() {
const [email, setEmail] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const register = useAuthStore(s =&gt; s.register)
const navigate = useNavigate()
&nbsp;
const handleSubmit = async (e) =&gt; {
e.preventDefault()
setError('')
setLoading(true)
&nbsp;
try {
await register(email, username, password)
navigate('/')
} catch (err) {
setError(err.response?.data?.error || 'Ошибка регистрации')
} finally {
setLoading(false)
}
}
&nbsp;
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-primary-50 via-white to-accent-50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 transition-colors duration-300"&gt;
&lt;motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-md"&gt;
&lt;div className="text-center mb-8"&gt;
&lt;motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', delay: 0.1 }} className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-accent-500 mb-4"&gt;
&lt;Sparkles className="w-8 h-8 text-white" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-2xl font-bold text-gray-900 dark:text-white"&gt;Создай аккаунт&lt;/h1&gt;
&lt;p className="text-gray-500 dark:text-gray-400 mt-1"&gt;Начни отслеживать свои привычки&lt;/p&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="card p-6"&gt;
&lt;form onSubmit={handleSubmit} className="space-y-4"&gt;
{error &amp;&amp; (
&lt;motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="p-3 rounded-xl bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm"&gt;
{error}
&lt;/motion.div&gt;
)}
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"&gt;Как тебя зовут?&lt;/label&gt;
&lt;input type="text" value={username} onChange={(e) =&gt; setUsername(e.target.value)} className="input" placeholder="Имя" required /&gt;
&lt;/div&gt;
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"&gt;Email&lt;/label&gt;
&lt;input type="email" value={email} onChange={(e) =&gt; setEmail(e.target.value)} className="input" placeholder="your@email.com" required /&gt;
&lt;/div&gt;
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"&gt;Пароль&lt;/label&gt;
&lt;div className="relative"&gt;
&lt;input type={showPassword ? 'text' : 'password'} value={password} onChange={(e) =&gt; setPassword(e.target.value)} className="input pr-12" placeholder="Минимум 8 символов" minLength={8} required /&gt;
&lt;button type="button" onClick={() =&gt; setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"&gt;
{showPassword ? &lt;EyeOff size={20} /&gt; : &lt;Eye size={20} /&gt;}
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
&lt;button type="submit" disabled={loading} className="btn btn-primary w-full"&gt;
{loading ? 'Создаём...' : 'Создать аккаунт'}
&lt;/button&gt;
&lt;/form&gt;
&nbsp;
&lt;p className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6"&gt;
Уже есть аккаунт?{' '}&lt;Link to="/login" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 font-medium"&gt;Войти&lt;/Link&gt;
&lt;/p&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,505 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/ResetPassword.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> ResetPassword.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">96.29% </span>
<span class="quiet">Statements</span>
<span class='fraction'>26/27</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">92.85% </span>
<span class="quiet">Branches</span>
<span class='fraction'>13/14</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">80% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>26/26</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
import { useSearchParams, Link, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Eye, EyeOff, Zap, CheckCircle } from 'lucide-react'
import api from '../api/client'
&nbsp;
export default function ResetPassword() {
const [searchParams] = useSearchParams()
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
const navigate = useNavigate()
const token = searchParams.get('token')
&nbsp;
const handleSubmit = async (e) =&gt; {
e.preventDefault()
if (!token) {
setError('Токен не найден')
return
}
&nbsp;
setError('')
setLoading(true)
&nbsp;
try {
await api.post('/auth/reset-password', { token, new_password: password })
setSuccess(true)
<span class="fstat-no" title="function not covered" > setTimeout(() =&gt; <span class="cstat-no" title="statement not covered" >n</span>avigate('/login'),</span> 2000)
} catch (err) {
setError(err.response?.data?.error || <span class="branch-1 cbranch-no" title="branch not covered" >'Ошибка сброса пароля')</span>
} finally {
setLoading(false)
}
}
&nbsp;
if (success) {
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50"&gt;
&lt;motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="card p-10 text-center max-w-md w-full"
&gt;
&lt;motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }}
className="w-20 h-20 rounded-3xl bg-green-100 flex items-center justify-center mx-auto mb-6"
&gt;
&lt;CheckCircle className="w-10 h-10 text-green-600" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-2xl font-display font-bold text-gray-900 mb-2"&gt;
Пароль изменён! 🎉
&lt;/h1&gt;
&lt;p className="text-gray-500"&gt;Перенаправляем на страницу входа...&lt;/p&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50"&gt;
&lt;motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md"
&gt;
&lt;div className="text-center mb-8"&gt;
&lt;motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: 0.1 }}
className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-500 to-primary-700 mb-6 shadow-xl shadow-primary-500/30"
&gt;
&lt;Zap className="w-10 h-10 text-white" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-3xl font-display font-bold text-gray-900"&gt;
Новый пароль
&lt;/h1&gt;
&lt;p className="text-gray-500 mt-2"&gt;Придумай новый надёжный пароль&lt;/p&gt;
&lt;/div&gt;
&nbsp;
&lt;div className="card p-8"&gt;
&lt;form onSubmit={handleSubmit} className="space-y-5"&gt;
{error &amp;&amp; (
&lt;motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="p-4 rounded-2xl bg-red-50 text-red-600 text-sm font-medium"
&gt;
{error}
&lt;/motion.div&gt;
)}
&nbsp;
&lt;div&gt;
&lt;label className="block text-sm font-semibold text-gray-700 mb-2"&gt;
Новый пароль
&lt;/label&gt;
&lt;div className="relative"&gt;
&lt;input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) =&gt; setPassword(e.target.value)}
className="input pr-12"
placeholder="Минимум 8 символов"
minLength={8}
required
/&gt;
&lt;button
type="button"
onClick={() =&gt; setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
&gt;
{showPassword ? &lt;EyeOff size={20} /&gt; : &lt;Eye size={20} /&gt;}
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&nbsp;
&lt;button
type="submit"
disabled={loading}
className="btn btn-primary w-full text-lg"
&gt;
{loading ? 'Сохраняем...' : 'Сохранить пароль'}
&lt;/button&gt;
&lt;/form&gt;
&nbsp;
&lt;div className="mt-6 text-center"&gt;
&lt;Link to="/login" className="text-primary-600 hover:text-primary-700 font-medium text-sm"&gt;
Вернуться ко входу
&lt;/Link&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,823 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/Tasks.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Tasks.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">50% </span>
<span class="quiet">Statements</span>
<span class='fraction'>28/56</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">58.66% </span>
<span class="quiet">Branches</span>
<span class='fraction'>44/75</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">31.81% </span>
<span class="quiet">Functions</span>
<span class='fraction'>7/22</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">60% </span>
<span class="quiet">Lines</span>
<span class='fraction'>27/45</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import { Plus, Check, Circle, Calendar, AlertTriangle, Undo2, Edit2, Repeat } from 'lucide-react'
import { format, parseISO, isToday, isTomorrow, isPast } from 'date-fns'
import { ru } from 'date-fns/locale'
import { tasksApi } from '../api/tasks'
import Navigation from '../components/Navigation'
import CreateTaskModal from '../components/CreateTaskModal'
import EditTaskModal from '../components/EditTaskModal'
import clsx from 'clsx'
&nbsp;
const PRIORITY_LABELS = {
0: null,
1: { label: 'Низкий', class: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' },
2: { label: 'Средний', class: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' },
3: { label: 'Высокий', class: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' },
}
&nbsp;
const RECURRENCE_LABELS = {
daily: 'Ежедневно',
weekly: 'Еженедельно',
monthly: 'Ежемесячно',
custom: 'Повтор',
}
&nbsp;
function formatDueDate(dateStr) {
if (!dateStr) return null
const date = parseISO(dateStr)
<span class="missing-if-branch" title="if path not taken" >I</span>if (isToday(date)) <span class="cstat-no" title="statement not covered" >return 'Сегодня'</span>
<span class="missing-if-branch" title="if path not taken" >I</span>if (isTomorrow(date)) <span class="cstat-no" title="statement not covered" >return 'Завтра'</span>
return format(date, 'd MMM', { locale: ru })
}
&nbsp;
export default function Tasks({ embedded = false }) {
const [showCreate, setShowCreate] = useState(false)
const [editingTask, setEditingTask] = useState(null)
const [filter, setFilter] = useState('active')
const queryClient = useQueryClient()
&nbsp;
const { data: tasks = [], isLoading } = useQuery({
queryKey: ['tasks', filter],
queryFn: () =&gt; {
<span class="missing-if-branch" title="if path not taken" >I</span>if (filter === 'all') <span class="cstat-no" title="statement not covered" >return tasksApi.list()</span>
return tasksApi.list(filter === 'completed')
},
})
&nbsp;
const completeMutation = useMutation({
<span class="fstat-no" title="function not covered" > mutationFn: (i</span>d) =&gt; <span class="cstat-no" title="statement not covered" >tasksApi.complete(id),</span>
<span class="fstat-no" title="function not covered" > onSuccess: () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks'] })</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks-today'] })</span>
},
})
&nbsp;
const uncompleteMutation = useMutation({
<span class="fstat-no" title="function not covered" > mutationFn: (i</span>d) =&gt; <span class="cstat-no" title="statement not covered" >tasksApi.uncomplete(id),</span>
<span class="fstat-no" title="function not covered" > onSuccess: () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks'] })</span>
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks-today'] })</span>
},
})
&nbsp;
const <span class="fstat-no" title="function not covered" >handleToggle = (t</span>ask) =&gt; {
<span class="cstat-no" title="statement not covered" > if (task.completed) <span class="cstat-no" title="statement not covered" >uncompleteMutation.mutate(task.id)</span></span>
else <span class="cstat-no" title="statement not covered" >completeMutation.mutate(task.id)</span>
}
&nbsp;
return (
&lt;div className={embedded ? "" : "min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"}&gt;
{!embedded &amp;&amp; &lt;header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"&gt;
&lt;div className="max-w-lg mx-auto px-4 py-4"&gt;
&lt;div className="flex items-center justify-between"&gt;
&lt;h1 className="text-xl font-display font-bold text-gray-900 dark:text-white"&gt;Задачи&lt;/h1&gt;
&lt;button <span class="fstat-no" title="function not covered" >onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowCreate(true)}</span> className="p-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors shadow-lg shadow-primary-500/30"&gt;
&lt;Plus size={22} /&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div className="flex gap-2 mt-4"&gt;
{[
{ key: 'active', label: 'Активные' },
{ key: 'completed', label: 'Выполненные' },
{ key: 'all', label: 'Все' },
].map(({ key, label }) =&gt; (
&lt;button
key={key}
<span class="fstat-no" title="function not covered" > onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etFilter(key)}</span>
className={clsx(
'px-4 py-2 rounded-xl text-sm font-medium transition-all',
filter === key
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/30'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
)}
&gt;
{label}
&lt;/button&gt;
))}
&lt;/div&gt;
&lt;/div&gt;
&lt;/header&gt;}
&nbsp;
&lt;main className="max-w-lg mx-auto px-4 py-6"&gt;
{isLoading ? (
&lt;div className="space-y-4"&gt;
{[1, 2, 3].map((i) =&gt; (
&lt;div key={i} className="card p-5 animate-pulse"&gt;
&lt;div className="flex items-center gap-4"&gt;
&lt;div className="w-10 h-10 rounded-xl bg-gray-200 dark:bg-gray-700" /&gt;
&lt;div className="flex-1"&gt;
&lt;div className="h-5 bg-gray-200 dark:bg-gray-700 rounded-lg w-3/4 mb-2" /&gt;
&lt;div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-lg w-1/4" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
))}
&lt;/div&gt;
) : tasks.length === 0 ? (
&lt;motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="card p-10 text-center"&gt;
&lt;div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-100 to-primary-200 dark:from-primary-900/30 dark:to-primary-800/30 flex items-center justify-center mx-auto mb-5"&gt;
&lt;Check className="w-10 h-10 text-primary-600 dark:text-primary-400" /&gt;
&lt;/div&gt;
&lt;h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2"&gt;
{filter === 'active' ? 'Нет активных задач' : <span class="branch-1 cbranch-no" title="branch not covered" >filter === 'completed' ? 'Нет выполненных задач' : 'Нет задач'}</span>
&lt;/h3&gt;
&lt;p className="text-gray-500 dark:text-gray-400 mb-6"&gt;
{filter === 'active' ? 'Добавь новую задачу или выбери другой фильтр' : <span class="branch-1 cbranch-no" title="branch not covered" >'Выполняй задачи и они появятся здесь'}</span>
&lt;/p&gt;
{filter === 'active' &amp;&amp; (
&lt;button <span class="fstat-no" title="function not covered" >onClick={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowCreate(true)}</span> className="btn btn-primary"&gt;
&lt;Plus size={18} /&gt;
Добавить задачу
&lt;/button&gt;
)}
&lt;/motion.div&gt;
) : (
&lt;div className="space-y-4"&gt;
&lt;AnimatePresence&gt;
{tasks.map((task, index) =&gt; (
&lt;TaskCard key={task.id} task={task} index={index} <span class="fstat-no" title="function not covered" >onToggle={() =&gt; <span class="cstat-no" title="statement not covered" >h</span>andleToggle(task)}</span> <span class="fstat-no" title="function not covered" >onEdit={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etEditingTask(task)}</span> isLoading={completeMutation.isPending || uncompleteMutation.isPending} /&gt;
))}
&lt;/AnimatePresence&gt;
&lt;/div&gt;
)}
&lt;/main&gt;
&nbsp;
{!embedded &amp;&amp; &lt;Navigation /&gt;}
&lt;CreateTaskModal open={showCreate} <span class="fstat-no" title="function not covered" >onClose={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowCreate(false)} /&gt;</span>
&lt;EditTaskModal open={!!editingTask} <span class="fstat-no" title="function not covered" >onClose={() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etEditingTask(null)}</span> task={editingTask} /&gt;
&lt;/div&gt;
)
}
&nbsp;
function TaskCard({ task, index, onToggle, onEdit, isLoading }) {
const [showConfetti, setShowConfetti] = useState(false)
const priorityInfo = PRIORITY_LABELS[task.priority]
const dueDateLabel = formatDueDate(task.due_date)
const isOverdue = task.due_date &amp;&amp; isPast(parseISO(task.due_date)) &amp;&amp; <span class="branch-2 cbranch-no" title="branch not covered" >!isToday(parseISO(task.due_date)) </span>&amp;&amp; <span class="branch-3 cbranch-no" title="branch not covered" >!task.completed</span>
&nbsp;
const <span class="fstat-no" title="function not covered" >handleCheck = (e</span>) =&gt; {
<span class="cstat-no" title="statement not covered" > e.stopPropagation()</span>
<span class="cstat-no" title="statement not covered" > if (isLoading) <span class="cstat-no" title="statement not covered" >return</span></span>
<span class="cstat-no" title="statement not covered" > if (!task.completed) { <span class="cstat-no" title="statement not covered" >setShowConfetti(true); <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etTimeout(() =&gt; <span class="cstat-no" title="statement not covered" >s</span>etShowConfetti(false),</span> 1000) }</span></span>
<span class="cstat-no" title="statement not covered" > onToggle()</span>
}
&nbsp;
return (
&lt;motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ delay: index * 0.05 }}
className="card p-4 relative overflow-hidden"
&gt;
{showConfetti &amp;&amp; (
<span class="branch-1 cbranch-no" title="branch not covered" > &lt;motion.div initial={{ opacity: 1 }} animate={{ opacity: 0 }} transition={{ duration: 1 }} className="absolute inset-0 pointer-events-none"&gt;</span>
{[...Array(6)].<span class="fstat-no" title="function not covered" >map((_</span>, i) =&gt; (
<span class="cstat-no" title="statement not covered" > &lt;motion.div key={i} initial={{ x: '50%', y: '50%', scale: 0 }} animate={{ x: `${Math.random() * 100}%`, y: `${Math.random() * 100}%`, scale: [0, 1, 0] }} transition={{ duration: 0.6, delay: i * 0.05 }} className="absolute w-2 h-2 rounded-full" style={{ backgroundColor: task.color }} /&gt;</span>
))}
&lt;/motion.div&gt;
)}
&lt;div className="flex items-start gap-3"&gt;
&lt;motion.button
onClick={handleCheck}
disabled={isLoading}
whileTap={{ scale: 0.9 }}
className={clsx(
'w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 flex-shrink-0 mt-0.5',
task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >'bg-gradient-to-br from-green-400 to-green-500 shadow-lg shadow-green-400/30' : '</span>border-2 hover:shadow-md'
)}
style={{ borderColor: task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >undefined : t</span>ask.color + '40', backgroundColor: task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >undefined : t</span>ask.color + '10' }}
&gt;
{task.completed ? (
<span class="branch-0 cbranch-no" title="branch not covered" > &lt;motion.div initial={{ scale: 0, rotate: -180 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: 'spring', stiffness: 500 }}&gt;</span>
&lt;Check className="w-5 h-5 text-white" strokeWidth={3} /&gt;
&lt;/motion.div&gt;
) : (
&lt;span className="text-lg"&gt;{task.icon || <span class="branch-1 cbranch-no" title="branch not covered" >'📋'}&lt;/span&gt;</span>
)}
&lt;/motion.button&gt;
&lt;div className="flex-1 min-w-0" onClick={onEdit}&gt;
&lt;div className="flex items-center gap-2"&gt;
&lt;h3 className={clsx("font-semibold truncate cursor-pointer hover:text-primary-600 dark:hover:text-primary-400", task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >"text-gray-400 line-through" : "</span>text-gray-900 dark:text-white")}&gt;{task.title}&lt;/h3&gt;
{task.is_recurring &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >&lt;span className="text-sm" title={RECURRENCE_LABELS[task.recurrence_type] || 'Повторяется'}&gt;🔄&lt;/span&gt;}</span>
&lt;/div&gt;
{task.description &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >&lt;p className="text-sm text-gray-500 dark:text-gray-400 truncate mt-0.5"&gt;{task.description}&lt;/p&gt;}</span>
&lt;div className="flex items-center gap-2 mt-2 flex-wrap"&gt;
{dueDateLabel &amp;&amp; (
&lt;span className={clsx('inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium', isOverdue ? <span class="branch-0 cbranch-no" title="branch not covered" >'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : '</span>bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400')}&gt;
{isOverdue &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >&lt;AlertTriangle size={12} /&gt;}</span>
&lt;Calendar size={12} /&gt;
{dueDateLabel}
&lt;/span&gt;
)}
{priorityInfo &amp;&amp; &lt;span className={clsx('px-2 py-0.5 rounded-md text-xs font-medium', priorityInfo.class)}&gt;{priorityInfo.label}&lt;/span&gt;}
{task.is_recurring &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >task.recurrence_type &amp;&amp; (</span>
<span class="branch-2 cbranch-no" title="branch not covered" > &lt;span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"&gt;</span>
&lt;Repeat size={12} /&gt;
{RECURRENCE_LABELS[task.recurrence_type]}
&lt;/span&gt;
)}
&lt;/div&gt;
&lt;/div&gt;
&lt;div className="flex items-center gap-1"&gt;
&lt;button onClick={onEdit} className="p-2 text-gray-400 hover:text-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-xl transition-all"&gt;
&lt;Edit2 size={16} /&gt;
&lt;/button&gt;
{task.completed &amp;&amp; (
<span class="branch-1 cbranch-no" title="branch not covered" > &lt;motion.button initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} onClick={handleCheck} disabled={isLoading} className="p-2 text-gray-400 hover:text-orange-500 hover:bg-orange-50 dark:hover:bg-orange-900/20 rounded-xl transition-all" title="Отменить"&gt;</span>
&lt;Undo2 size={16} /&gt;
&lt;/motion.button&gt;
)}
&lt;/div&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,241 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/Tracker.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Tracker.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>5/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>8/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>5/5</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useState, lazy, Suspense } from "react"
import Navigation from "../components/Navigation"
&nbsp;
// Import pages as components (they render their own content but we strip their Navigation)
import HabitsContent from "./Habits"
import TasksContent from "./Tasks"
import StatsContent from "./Stats"
&nbsp;
const tabs = [
{ key: "habits", label: "Привычки", icon: "🎯" },
{ key: "tasks", label: "Задачи", icon: "✅" },
{ key: "stats", label: "Статистика", icon: "📊" },
]
&nbsp;
export default function Tracker() {
const [activeTab, setActiveTab] = useState("habits")
&nbsp;
return (
&lt;div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24"&gt;
&lt;header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"&gt;
&lt;div className="max-w-lg mx-auto px-4 py-4"&gt;
&lt;h1 className="text-xl font-display font-bold text-gray-900 dark:text-white"&gt;
📊 Трекер
&lt;/h1&gt;
&lt;/div&gt;
&lt;div className="max-w-lg mx-auto px-4 pb-3 flex gap-2"&gt;
{tabs.map((t) =&gt; (
&lt;button
key={t.key}
onClick={() =&gt; setActiveTab(t.key)}
className={`flex-1 py-2 rounded-xl text-sm font-semibold transition ${
activeTab === t.key
? "bg-primary-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
&gt;
{t.icon} {t.label}
&lt;/button&gt;
))}
&lt;/div&gt;
&lt;/header&gt;
&nbsp;
&lt;div&gt;
{activeTab === "habits" &amp;&amp; &lt;HabitsContent embedded /&gt;}
{activeTab === "tasks" &amp;&amp; &lt;TasksContent embedded /&gt;}
{activeTab === "stats" &amp;&amp; &lt;StatsContent embedded /&gt;}
&lt;/div&gt;
&nbsp;
&lt;Navigation /&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,394 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages/VerifyEmail.jsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> VerifyEmail.jsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>18/18</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90% </span>
<span class="quiet">Branches</span>
<span class='fraction'>9/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>18/18</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useState } from 'react'
import { useSearchParams, Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { CheckCircle, XCircle, Loader2, Zap } from 'lucide-react'
import api from '../api/client'
&nbsp;
export default function VerifyEmail() {
const [searchParams] = useSearchParams()
const [status, setStatus] = useState('loading') // loading, success, error
const [message, setMessage] = useState('')
const token = searchParams.get('token')
&nbsp;
useEffect(() =&gt; {
if (!token) {
setStatus('error')
setMessage('Токен не найден')
return
}
&nbsp;
const verify = async () =&gt; {
try {
await api.post('/auth/verify-email', { token })
setStatus('success')
setMessage('Email успешно подтверждён!')
} catch (err) {
setStatus('error')
setMessage(err.response?.data?.error || <span class="branch-1 cbranch-no" title="branch not covered" >'Ошибка верификации')</span>
}
}
&nbsp;
verify()
}, [token])
&nbsp;
return (
&lt;div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50"&gt;
&lt;motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md"
&gt;
&lt;div className="card p-10 text-center"&gt;
{status === 'loading' &amp;&amp; (
&lt;&gt;
&lt;div className="w-20 h-20 rounded-3xl bg-primary-100 flex items-center justify-center mx-auto mb-6"&gt;
&lt;Loader2 className="w-10 h-10 text-primary-600 animate-spin" /&gt;
&lt;/div&gt;
&lt;h1 className="text-2xl font-display font-bold text-gray-900 mb-2"&gt;
Проверяем...
&lt;/h1&gt;
&lt;p className="text-gray-500"&gt;Подожди секунду&lt;/p&gt;
&lt;/&gt;
)}
&nbsp;
{status === 'success' &amp;&amp; (
&lt;&gt;
&lt;motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }}
className="w-20 h-20 rounded-3xl bg-green-100 flex items-center justify-center mx-auto mb-6"
&gt;
&lt;CheckCircle className="w-10 h-10 text-green-600" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-2xl font-display font-bold text-gray-900 mb-2"&gt;
Готово! 🎉
&lt;/h1&gt;
&lt;p className="text-gray-500 mb-6"&gt;{message}&lt;/p&gt;
&lt;Link to="/login" className="btn btn-primary"&gt;
Войти в аккаунт
&lt;/Link&gt;
&lt;/&gt;
)}
&nbsp;
{status === 'error' &amp;&amp; (
&lt;&gt;
&lt;motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }}
className="w-20 h-20 rounded-3xl bg-red-100 flex items-center justify-center mx-auto mb-6"
&gt;
&lt;XCircle className="w-10 h-10 text-red-600" /&gt;
&lt;/motion.div&gt;
&lt;h1 className="text-2xl font-display font-bold text-gray-900 mb-2"&gt;
Ошибка
&lt;/h1&gt;
&lt;p className="text-gray-500 mb-6"&gt;{message}&lt;/p&gt;
&lt;Link to="/login" className="btn btn-secondary"&gt;
На главную
&lt;/Link&gt;
&lt;/&gt;
)}
&lt;/div&gt;
&nbsp;
&lt;div className="flex items-center justify-center gap-2 mt-6 text-gray-400"&gt;
&lt;Zap size={16} /&gt;
&lt;span className="text-sm font-medium"&gt;Pulse&lt;/span&gt;
&lt;/div&gt;
&lt;/motion.div&gt;
&lt;/div&gt;
)
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,296 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for pages</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> pages</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">46.79% </span>
<span class="quiet">Statements</span>
<span class='fraction'>452/966</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">40.62% </span>
<span class="quiet">Branches</span>
<span class='fraction'>325/800</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">32.11% </span>
<span class="quiet">Functions</span>
<span class='fraction'>105/327</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">50.95% </span>
<span class="quiet">Lines</span>
<span class='fraction'>427/838</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file low" data-value="Finance.jsx"><a href="Finance.jsx.html">Finance.jsx</a></td>
<td data-value="44.44" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 44%"></div><div class="cover-empty" style="width: 56%"></div></div>
</td>
<td data-value="44.44" class="pct low">44.44%</td>
<td data-value="36" class="abs low">16/36</td>
<td data-value="75" class="pct medium">75%</td>
<td data-value="20" class="abs medium">15/20</td>
<td data-value="25" class="pct low">25%</td>
<td data-value="16" class="abs low">4/16</td>
<td data-value="64" class="pct medium">64%</td>
<td data-value="25" class="abs medium">16/25</td>
</tr>
<tr>
<td class="file high" data-value="ForgotPassword.jsx"><a href="ForgotPassword.jsx.html">ForgotPassword.jsx</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="17" class="abs high">17/17</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="17" class="abs high">17/17</td>
</tr>
<tr>
<td class="file medium" data-value="Habits.jsx"><a href="Habits.jsx.html">Habits.jsx</a></td>
<td data-value="64.91" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 64%"></div><div class="cover-empty" style="width: 36%"></div></div>
</td>
<td data-value="64.91" class="pct medium">64.91%</td>
<td data-value="57" class="abs medium">37/57</td>
<td data-value="65.9" class="pct medium">65.9%</td>
<td data-value="44" class="abs medium">29/44</td>
<td data-value="53.57" class="pct medium">53.57%</td>
<td data-value="28" class="abs medium">15/28</td>
<td data-value="65.21" class="pct medium">65.21%</td>
<td data-value="46" class="abs medium">30/46</td>
</tr>
<tr>
<td class="file low" data-value="Home.jsx"><a href="Home.jsx.html">Home.jsx</a></td>
<td data-value="17.46" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 17%"></div><div class="cover-empty" style="width: 83%"></div></div>
</td>
<td data-value="17.46" class="pct low">17.46%</td>
<td data-value="189" class="abs low">33/189</td>
<td data-value="8.84" class="pct low">8.84%</td>
<td data-value="147" class="abs low">13/147</td>
<td data-value="9.25" class="pct low">9.25%</td>
<td data-value="54" class="abs low">5/54</td>
<td data-value="21.15" class="pct low">21.15%</td>
<td data-value="156" class="abs low">33/156</td>
</tr>
<tr>
<td class="file high" data-value="Login.jsx"><a href="Login.jsx.html">Login.jsx</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="21" class="abs high">21/21</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="6" class="abs high">6/6</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="20" class="abs high">20/20</td>
</tr>
<tr>
<td class="file high" data-value="Register.jsx"><a href="Register.jsx.html">Register.jsx</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="23" class="abs high">23/23</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="7" class="abs high">7/7</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="22" class="abs high">22/22</td>
</tr>
<tr>
<td class="file high" data-value="ResetPassword.jsx"><a href="ResetPassword.jsx.html">ResetPassword.jsx</a></td>
<td data-value="96.29" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 96%"></div><div class="cover-empty" style="width: 4%"></div></div>
</td>
<td data-value="96.29" class="pct high">96.29%</td>
<td data-value="27" class="abs high">26/27</td>
<td data-value="92.85" class="pct high">92.85%</td>
<td data-value="14" class="abs high">13/14</td>
<td data-value="80" class="pct high">80%</td>
<td data-value="5" class="abs high">4/5</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="26" class="abs high">26/26</td>
</tr>
<tr>
<td class="file low" data-value="Savings.jsx"><a href="Savings.jsx.html">Savings.jsx</a></td>
<td data-value="15.88" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 15%"></div><div class="cover-empty" style="width: 85%"></div></div>
</td>
<td data-value="15.88" class="pct low">15.88%</td>
<td data-value="277" class="abs low">44/277</td>
<td data-value="14.07" class="pct low">14.07%</td>
<td data-value="270" class="abs low">38/270</td>
<td data-value="9.09" class="pct low">9.09%</td>
<td data-value="121" class="abs low">11/121</td>
<td data-value="17.4" class="pct low">17.4%</td>
<td data-value="247" class="abs low">43/247</td>
</tr>
<tr>
<td class="file high" data-value="Settings.jsx"><a href="Settings.jsx.html">Settings.jsx</a></td>
<td data-value="81.63" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 81%"></div><div class="cover-empty" style="width: 19%"></div></div>
</td>
<td data-value="81.63" class="pct high">81.63%</td>
<td data-value="49" class="abs high">40/49</td>
<td data-value="78.18" class="pct medium">78.18%</td>
<td data-value="55" class="abs medium">43/55</td>
<td data-value="50" class="pct medium">50%</td>
<td data-value="14" class="abs medium">7/14</td>
<td data-value="83.33" class="pct high">83.33%</td>
<td data-value="48" class="abs high">40/48</td>
</tr>
<tr>
<td class="file medium" data-value="Stats.jsx"><a href="Stats.jsx.html">Stats.jsx</a></td>
<td data-value="75.39" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 75%"></div><div class="cover-empty" style="width: 25%"></div></div>
</td>
<td data-value="75.39" class="pct medium">75.39%</td>
<td data-value="191" class="abs medium">144/191</td>
<td data-value="65.89" class="pct medium">65.89%</td>
<td data-value="129" class="abs medium">85/129</td>
<td data-value="66.66" class="pct medium">66.66%</td>
<td data-value="45" class="abs medium">30/45</td>
<td data-value="79.75" class="pct medium">79.75%</td>
<td data-value="163" class="abs medium">130/163</td>
</tr>
<tr>
<td class="file medium" data-value="Tasks.jsx"><a href="Tasks.jsx.html">Tasks.jsx</a></td>
<td data-value="50" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 50%"></div><div class="cover-empty" style="width: 50%"></div></div>
</td>
<td data-value="50" class="pct medium">50%</td>
<td data-value="56" class="abs medium">28/56</td>
<td data-value="58.66" class="pct medium">58.66%</td>
<td data-value="75" class="abs medium">44/75</td>
<td data-value="31.81" class="pct low">31.81%</td>
<td data-value="22" class="abs low">7/22</td>
<td data-value="60" class="pct medium">60%</td>
<td data-value="45" class="abs medium">27/45</td>
</tr>
<tr>
<td class="file high" data-value="Tracker.jsx"><a href="Tracker.jsx.html">Tracker.jsx</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
</tr>
<tr>
<td class="file high" data-value="VerifyEmail.jsx"><a href="VerifyEmail.jsx.html">VerifyEmail.jsx</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="18" class="abs high">18/18</td>
<td data-value="90" class="pct high">90%</td>
<td data-value="10" class="abs high">9/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="18" class="abs high">18/18</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1 +0,0 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

View File

@@ -1,210 +0,0 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
// it will be treated as a plain text search
let searchRegex;
try {
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
} catch (error) {
searchRegex = null;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
let isMatch = false;
if (searchRegex) {
// If a valid regex was created, use it for matching
isMatch = searchRegex.test(row.textContent);
} else {
// Otherwise, fall back to the original plain text search
isMatch = row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase());
}
row.style.display = isMatch ? '' : 'none';
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

@@ -1,226 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for store/auth.js</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">store</a> auth.js</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>25/25</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>2/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>5/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>24/24</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { create } from 'zustand'
import api from '../api/client'
&nbsp;
export const useAuthStore = create((set, get) =&gt; ({
user: null,
isLoading: true,
isAuthenticated: false,
&nbsp;
initialize: async () =&gt; {
const token = localStorage.getItem('access_token')
if (!token) {
set({ isLoading: false, isAuthenticated: false })
return
}
&nbsp;
try {
const { data } = await api.get('/auth/me')
set({ user: data, isLoading: false, isAuthenticated: true })
} catch (error) {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
set({ user: null, isLoading: false, isAuthenticated: false })
}
},
&nbsp;
login: async (email, password) =&gt; {
const { data } = await api.post('/auth/login', { email, password })
localStorage.setItem('access_token', data.access_token)
localStorage.setItem('refresh_token', data.refresh_token)
set({ user: data.user, isAuthenticated: true })
return data
},
&nbsp;
register: async (email, username, password) =&gt; {
const { data } = await api.post('/auth/register', { email, username, password })
localStorage.setItem('access_token', data.access_token)
localStorage.setItem('refresh_token', data.refresh_token)
set({ user: data.user, isAuthenticated: true })
return data
},
&nbsp;
logout: () =&gt; {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
set({ user: null, isAuthenticated: false })
},
}))
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

View File

@@ -1,116 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for store</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> store</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>25/25</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>2/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>5/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>24/24</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="auth.js"><a href="auth.js.html">auth.js</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="25" class="abs high">25/25</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="24" class="abs high">24/24</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-03-26T19:16:45.407Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import ResetPassword from "./pages/ResetPassword"
import ForgotPassword from "./pages/ForgotPassword"
import Stats from "./pages/Stats"
import Settings from "./pages/Settings"
import Finance from "./pages/Finance"
import Tracker from "./pages/Tracker"
function ProtectedRoute({ children }) {
@@ -134,6 +135,14 @@ export default function App() {
</ProtectedRoute>
}
/>
<Route
path="/finance"
element={
<ProtectedRoute>
<Finance />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={

View File

@@ -1,7 +0,0 @@
import { describe, it, expect, vi } from 'vitest'
describe('App', () => {
it('should pass basic test', () => {
expect(1 + 1).toBe(2)
})
})

View File

@@ -1,100 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import CreateHabitModal from '../components/CreateHabitModal'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
},
}))
vi.mock('../api/habits', () => ({
habitsApi: {
create: vi.fn(),
list: vi.fn(),
},
}))
import { habitsApi } from '../api/habits'
const renderModal = (props = {}) => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<CreateHabitModal open={true} onClose={vi.fn()} {...props} />
</QueryClientProvider>
)
}
describe('CreateHabitModal', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('does not render when open=false', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<CreateHabitModal open={false} onClose={vi.fn()} />
</QueryClientProvider>
)
expect(screen.queryByText('Новая привычка')).not.toBeInTheDocument()
})
it('renders form when open=true', () => {
renderModal()
expect(screen.getByText('Новая привычка')).toBeInTheDocument()
// Placeholder is "Например: Пить воду"
expect(screen.getByPlaceholderText('Например: Пить воду')).toBeInTheDocument()
})
it('shows error when submitting empty name', async () => {
renderModal()
fireEvent.click(screen.getByText('Создать привычку'))
await waitFor(() => {
expect(screen.getByText('Введи название привычки')).toBeInTheDocument()
})
})
it('submits habit successfully', async () => {
habitsApi.create.mockResolvedValueOnce({ id: 1, name: 'Exercise' })
const onClose = vi.fn()
renderModal({ onClose })
fireEvent.change(screen.getByPlaceholderText('Например: Пить воду'), {
target: { value: 'Exercise' },
})
fireEvent.click(screen.getByText('Создать привычку'))
await waitFor(() => {
expect(habitsApi.create).toHaveBeenCalled()
})
})
it('renders frequency options', () => {
renderModal()
expect(screen.getByText('Ежедневно')).toBeInTheDocument()
// "По дням" is shown instead of "Еженедельно"
expect(screen.getByText('По дням')).toBeInTheDocument()
})
it('calls onClose when close button clicked', () => {
const onClose = vi.fn()
renderModal({ onClose })
const closeBtn = screen.getAllByRole('button')[0]
fireEvent.click(closeBtn)
expect(onClose).toHaveBeenCalled()
})
it('renders color options', () => {
renderModal()
// Colors rendered as buttons
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThan(5)
})
})

View File

@@ -1,107 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import CreateTaskModal from '../components/CreateTaskModal'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
},
}))
vi.mock('../api/tasks', () => ({
tasksApi: {
create: vi.fn(),
list: vi.fn(),
},
}))
import { tasksApi } from '../api/tasks'
const renderModal = (props = {}) => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<CreateTaskModal open={true} onClose={vi.fn()} {...props} />
</QueryClientProvider>
)
}
describe('CreateTaskModal', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('does not render when open=false', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<CreateTaskModal open={false} onClose={vi.fn()} />
</QueryClientProvider>
)
expect(screen.queryByText('Новая задача')).not.toBeInTheDocument()
})
it('renders form when open=true', () => {
renderModal()
expect(screen.getByText('Новая задача')).toBeInTheDocument()
// Actual placeholder in component
expect(screen.getByPlaceholderText('Что нужно сделать?')).toBeInTheDocument()
})
it('shows error when submitting empty title', async () => {
renderModal()
fireEvent.click(screen.getByText('Создать задачу'))
await waitFor(() => {
expect(screen.getByText('Введи название задачи')).toBeInTheDocument()
})
})
it('submits task successfully', async () => {
tasksApi.create.mockResolvedValueOnce({ id: 1, title: 'Test Task' })
const onClose = vi.fn()
renderModal({ onClose })
fireEvent.change(screen.getByPlaceholderText('Что нужно сделать?'), {
target: { value: 'Test Task' },
})
fireEvent.click(screen.getByText('Создать задачу'))
await waitFor(() => {
expect(tasksApi.create).toHaveBeenCalled()
})
})
it('renders priority buttons', () => {
renderModal()
expect(screen.getByText('Без приоритета')).toBeInTheDocument()
expect(screen.getByText('Низкий')).toBeInTheDocument()
expect(screen.getByText('Средний')).toBeInTheDocument()
expect(screen.getByText('Высокий')).toBeInTheDocument()
})
it('renders color picker', () => {
renderModal()
// Colors are rendered as buttons/divs
const colorElements = document.querySelectorAll('[style*="background"]')
expect(colorElements.length).toBeGreaterThan(0)
})
it('calls onClose when X clicked', () => {
const onClose = vi.fn()
renderModal({ onClose })
const closeBtn = screen.getAllByRole('button')[0]
fireEvent.click(closeBtn)
expect(onClose).toHaveBeenCalled()
})
it('renders recurring toggle', () => {
renderModal()
// The label says "Повторять" in the component
expect(screen.getByText('Повторять')).toBeInTheDocument()
})
})

View File

@@ -1,124 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import EditHabitModal from '../components/EditHabitModal'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
},
}))
vi.mock('../api/habits', () => ({
habitsApi: {
update: vi.fn(),
delete: vi.fn(),
getFreezes: vi.fn().mockResolvedValue([]),
addFreeze: vi.fn(),
deleteFreeze: vi.fn(),
},
}))
import { habitsApi } from '../api/habits'
const mockHabit = {
id: 1,
name: 'Exercise',
description: 'Daily workout',
color: '#6366f1',
icon: '💪',
frequency: 'daily',
target_days: [],
target_count: 1,
reminder_time: null,
start_date: '2026-01-01',
}
const renderModal = (props = {}) => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<EditHabitModal open={true} onClose={vi.fn()} habit={mockHabit} {...props} />
</QueryClientProvider>
)
}
describe('EditHabitModal', () => {
beforeEach(() => {
vi.clearAllMocks()
habitsApi.getFreezes.mockResolvedValue([])
})
it('does not render when open=false', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<EditHabitModal open={false} onClose={vi.fn()} habit={mockHabit} />
</QueryClientProvider>
)
expect(screen.queryByText('Редактировать привычку')).not.toBeInTheDocument()
})
it('renders with habit data', () => {
renderModal()
expect(screen.getByText('Редактировать привычку')).toBeInTheDocument()
expect(screen.getByDisplayValue('Exercise')).toBeInTheDocument()
})
it('renders save button', () => {
renderModal()
// Button text is "Сохранить изменения"
expect(screen.getByText('Сохранить изменения')).toBeInTheDocument()
})
it('submits updated habit', async () => {
habitsApi.update.mockResolvedValueOnce({ id: 1, name: 'Updated' })
renderModal()
fireEvent.change(screen.getByDisplayValue('Exercise'), {
target: { value: 'Updated Exercise' },
})
fireEvent.click(screen.getByText('Сохранить изменения'))
await waitFor(() => {
expect(habitsApi.update).toHaveBeenCalled()
})
})
it('renders delete button', () => {
renderModal()
// Button text is "Удалить привычку"
expect(screen.getByText('Удалить привычку')).toBeInTheDocument()
})
it('shows delete confirmation when delete clicked', () => {
renderModal()
fireEvent.click(screen.getByText('Удалить привычку'))
// Confirmation shows "Удалить привычку?" and confirm button "Удалить"
expect(screen.getByText('Удалить привычку?')).toBeInTheDocument()
})
it('deletes habit on confirmation', async () => {
habitsApi.delete.mockResolvedValueOnce({})
renderModal()
fireEvent.click(screen.getByText('Удалить привычку'))
// Confirm button shows "Удалить"
const deleteBtn = screen.getAllByText('Удалить').find(el => el.tagName === 'BUTTON')
fireEvent.click(deleteBtn)
await waitFor(() => {
expect(habitsApi.delete).toHaveBeenCalledWith(1)
})
})
it('renders freezes section', async () => {
renderModal()
await waitFor(() => {
expect(screen.getByText(/Заморозки/)).toBeInTheDocument()
})
})
})

View File

@@ -1,111 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import EditTaskModal from '../components/EditTaskModal'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
},
}))
vi.mock('../api/tasks', () => ({
tasksApi: {
update: vi.fn(),
delete: vi.fn(),
},
}))
import { tasksApi } from '../api/tasks'
const mockTask = {
id: 1,
title: 'Test Task',
description: 'Description',
color: '#6366f1',
icon: '📋',
due_date: '2026-03-26',
priority: 1,
reminder_time: null,
is_recurring: false,
recurrence_type: null,
recurrence_interval: 1,
recurrence_end_date: null,
}
const renderModal = (props = {}) => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<EditTaskModal open={true} onClose={vi.fn()} task={mockTask} {...props} />
</QueryClientProvider>
)
}
describe('EditTaskModal', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('does not render when open=false', () => {
const qc = new QueryClient()
render(
<QueryClientProvider client={qc}>
<EditTaskModal open={false} onClose={vi.fn()} task={mockTask} />
</QueryClientProvider>
)
expect(screen.queryByText('Редактировать задачу')).not.toBeInTheDocument()
})
it('renders with task data pre-filled', () => {
renderModal()
const titleInput = screen.getByDisplayValue('Test Task')
expect(titleInput).toBeInTheDocument()
})
it('renders edit modal title', () => {
renderModal()
expect(screen.getByText('Редактировать задачу')).toBeInTheDocument()
})
it('submits updated task', async () => {
tasksApi.update.mockResolvedValueOnce({ id: 1, title: 'Updated' })
renderModal()
const titleInput = screen.getByDisplayValue('Test Task')
fireEvent.change(titleInput, { target: { value: 'Updated Task' } })
fireEvent.click(screen.getByText('Сохранить'))
await waitFor(() => {
expect(tasksApi.update).toHaveBeenCalled()
})
})
it('shows delete confirmation button', () => {
renderModal()
// Button says "Удалить задачу"
expect(screen.getByText('Удалить задачу')).toBeInTheDocument()
})
it('shows delete confirmation when delete clicked', () => {
renderModal()
fireEvent.click(screen.getByText('Удалить задачу'))
// Confirmation shows "Да, удалить"
expect(screen.getByText('Да, удалить')).toBeInTheDocument()
})
it('deletes task after confirmation', async () => {
tasksApi.delete.mockResolvedValueOnce({})
renderModal()
fireEvent.click(screen.getByText('Удалить задачу'))
fireEvent.click(screen.getByText('Да, удалить'))
await waitFor(() => {
expect(tasksApi.delete).toHaveBeenCalledWith(1)
})
})
})

View File

@@ -1,93 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import Finance from '../pages/Finance'
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
vi.mock('../components/finance/FinanceDashboard', () => ({
default: ({ month, year }) => <div data-testid="finance-dashboard">Dashboard {month}/{year}</div>,
}))
vi.mock('../components/finance/TransactionList', () => ({
default: ({ onAdd }) => (
<div data-testid="transaction-list">
<button onClick={onAdd}>Add</button>
</div>
),
}))
vi.mock('../components/finance/FinanceAnalytics', () => ({
default: () => <div data-testid="finance-analytics">Analytics</div>,
}))
vi.mock('../components/finance/CategoriesManager', () => ({
default: () => <div data-testid="categories-manager">Categories</div>,
}))
vi.mock('../components/finance/AddTransactionModal', () => ({
default: ({ onClose, onSaved }) => (
<div data-testid="add-transaction-modal">
<button onClick={onClose}>Close</button>
<button onClick={onSaved}>Save</button>
</div>
),
}))
describe('Finance page', () => {
it('renders finance page header', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
expect(screen.getByText('💰 Финансы')).toBeInTheDocument()
})
it('renders tab navigation', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
// Buttons contain emoji + text, use regex
expect(screen.getByText(/Обзор/)).toBeInTheDocument()
expect(screen.getByText(/Транзакции/)).toBeInTheDocument()
expect(screen.getByText(/Аналитика/)).toBeInTheDocument()
expect(screen.getByText(/Категории/)).toBeInTheDocument()
})
it('shows dashboard tab by default', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
expect(screen.getByTestId('finance-dashboard')).toBeInTheDocument()
})
it('switches to transactions tab', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
fireEvent.click(screen.getByText(/Транзакции/))
expect(screen.getByTestId('transaction-list')).toBeInTheDocument()
})
it('switches to analytics tab', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
fireEvent.click(screen.getByText(/Аналитика/))
expect(screen.getByTestId('finance-analytics')).toBeInTheDocument()
})
it('switches to categories tab', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
fireEvent.click(screen.getByText(/Категории/))
expect(screen.getByTestId('categories-manager')).toBeInTheDocument()
})
it('renders navigation', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
it('can navigate months', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
expect(screen.getByTestId('finance-dashboard')).toBeInTheDocument()
})
it('opens add transaction modal from transactions tab', () => {
render(<MemoryRouter><Finance /></MemoryRouter>)
fireEvent.click(screen.getByText(/Транзакции/))
fireEvent.click(screen.getByText('Add'))
expect(screen.getByTestId('add-transaction-modal')).toBeInTheDocument()
})
})

View File

@@ -1,72 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import FinanceDashboard from '../components/finance/FinanceDashboard'
vi.mock('recharts', () => ({
PieChart: ({ children }) => <div data-testid="pie-chart">{children}</div>,
Pie: () => <div />,
Cell: () => <div />,
LineChart: ({ children }) => <div data-testid="line-chart">{children}</div>,
Line: () => <div />,
XAxis: () => <div />,
YAxis: () => <div />,
Tooltip: () => <div />,
ResponsiveContainer: ({ children }) => <div>{children}</div>,
}))
vi.mock('../api/finance', () => ({
financeApi: {
getSummary: vi.fn(),
},
}))
import { financeApi } from '../api/finance'
describe('FinanceDashboard', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('shows loading state', () => {
financeApi.getSummary.mockReturnValue(new Promise(() => {}))
render(<FinanceDashboard month={3} year={2026} />)
const skeletons = document.querySelectorAll('.animate-pulse')
expect(skeletons.length).toBeGreaterThan(0)
})
it('shows empty state when no data', async () => {
financeApi.getSummary.mockResolvedValueOnce({
total_income: 0,
total_expense: 0,
balance: 0,
carried_over: 0,
by_category: [],
daily: [],
})
render(<FinanceDashboard month={3} year={2026} />)
await waitFor(() => {
expect(screen.getByText('Нет данных')).toBeInTheDocument()
})
})
it('shows dashboard when data available', async () => {
financeApi.getSummary.mockResolvedValueOnce({
total_income: 100000,
total_expense: 50000,
balance: 50000,
carried_over: 0,
by_category: [
{ category_name: 'Еда', category_emoji: '🍔', type: 'expense', amount: 10000 },
],
daily: [
{ date: '2026-03-01', income: 0, expense: 500 },
],
})
render(<FinanceDashboard month={3} year={2026} />)
await waitFor(() => {
// Use getAllByText since "50 000" appears multiple times
const elements = screen.getAllByText(/50\s*000/)
expect(elements.length).toBeGreaterThan(0)
})
})
})

View File

@@ -1,107 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import ForgotPassword from '../pages/ForgotPassword'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
interceptors: {
request: { use: vi.fn() },
response: { use: vi.fn() },
},
},
}))
import api from '../api/client'
describe('ForgotPassword page', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const renderPage = () => render(<MemoryRouter><ForgotPassword /></MemoryRouter>)
it('renders form', () => {
renderPage()
expect(screen.getByText('Забыли пароль?')).toBeInTheDocument()
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument()
expect(screen.getByText('Отправить ссылку')).toBeInTheDocument()
})
it('renders back to login link', () => {
renderPage()
expect(screen.getByText('Вернуться ко входу')).toBeInTheDocument()
})
it('shows success state after submit', async () => {
api.post.mockResolvedValueOnce({})
renderPage()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'test@test.com' },
})
fireEvent.click(screen.getByText('Отправить ссылку'))
await waitFor(() => {
expect(screen.getByText('Письмо отправлено! 📬')).toBeInTheDocument()
})
})
it('shows email in success state', async () => {
api.post.mockResolvedValueOnce({})
renderPage()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'myemail@test.com' },
})
fireEvent.click(screen.getByText('Отправить ссылку'))
await waitFor(() => {
expect(screen.getByText(/myemail@test\.com/)).toBeInTheDocument()
})
})
it('shows error on failure', async () => {
api.post.mockRejectedValueOnce({ response: { data: { error: 'Пользователь не найден' } } })
renderPage()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'bad@test.com' },
})
fireEvent.click(screen.getByText('Отправить ссылку'))
await waitFor(() => {
expect(screen.getByText('Пользователь не найден')).toBeInTheDocument()
})
})
it('shows default error message', async () => {
api.post.mockRejectedValueOnce(new Error('Network'))
renderPage()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'bad@test.com' },
})
fireEvent.click(screen.getByText('Отправить ссылку'))
await waitFor(() => {
expect(screen.getByText('Ошибка отправки')).toBeInTheDocument()
})
})
it('calls correct API endpoint', async () => {
api.post.mockResolvedValueOnce({})
renderPage()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'test@test.com' },
})
fireEvent.click(screen.getByText('Отправить ссылку'))
await waitFor(() => {
expect(api.post).toHaveBeenCalledWith('/auth/forgot-password', { email: 'test@test.com' })
})
})
})

View File

@@ -1,103 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Habits from '../pages/Habits'
vi.mock('../api/habits', () => ({
habitsApi: {
list: vi.fn(),
getHabitStats: vi.fn(),
update: vi.fn(),
},
}))
vi.mock('../components/CreateHabitModal', () => ({
default: ({ open, onClose }) => open ? (
<div data-testid="create-habit-modal">
<button onClick={onClose}>Close</button>
</div>
) : null,
}))
vi.mock('../components/EditHabitModal', () => ({
default: ({ open, onClose }) => open ? (
<div data-testid="edit-habit-modal">
<button onClick={onClose}>Close</button>
</div>
) : null,
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
import { habitsApi } from '../api/habits'
const mockHabits = [
{ id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
{ id: 2, name: 'Read', frequency: 'weekly', target_days: [1,2,3,4,5], color: '#22c55e', icon: '📚', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
]
const renderHabits = (embedded = false) => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>
<Habits embedded={embedded} />
</MemoryRouter>
</QueryClientProvider>
)
}
describe('Habits page', () => {
beforeEach(() => {
vi.clearAllMocks()
habitsApi.list.mockResolvedValue(mockHabits)
habitsApi.getHabitStats.mockResolvedValue({ streak: 5, completion_rate: 80 })
})
it('renders habits list', async () => {
renderHabits()
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
expect(screen.getByText('Read')).toBeInTheDocument()
})
})
it('renders header when not embedded', async () => {
renderHabits(false)
await waitFor(() => {
expect(screen.getByText('Мои привычки')).toBeInTheDocument()
})
})
it('does not render header when embedded', async () => {
renderHabits(true)
await waitFor(() => {
expect(screen.queryByText('Мои привычки')).not.toBeInTheDocument()
})
})
it('opens create habit modal', async () => {
renderHabits()
await waitFor(() => {
expect(screen.getByText('Мои привычки')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Новая'))
expect(screen.getByTestId('create-habit-modal')).toBeInTheDocument()
})
it('renders navigation when not embedded', () => {
renderHabits(false)
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
it('shows empty state when no habits', async () => {
habitsApi.list.mockResolvedValue([])
renderHabits()
await waitFor(() => {
expect(screen.getByText(/Нет привычек/)).toBeInTheDocument()
})
})
})

View File

@@ -1,397 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Home from '../pages/Home'
import { useAuthStore } from '../store/auth'
vi.mock('../store/auth', () => ({
useAuthStore: vi.fn(),
}))
vi.mock('../api/habits', () => ({
habitsApi: {
list: vi.fn(),
getLogs: vi.fn(),
log: vi.fn(),
getStats: vi.fn(),
getHabitStats: vi.fn(),
getFreezes: vi.fn(),
deleteLog: vi.fn(),
},
}))
vi.mock('../api/tasks', () => ({
tasksApi: {
today: vi.fn(),
complete: vi.fn(),
uncomplete: vi.fn(),
},
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
vi.mock('../components/CreateTaskModal', () => ({
default: ({ open, onClose }) => open ? <div data-testid="create-task-modal"><button onClick={onClose}>Close</button></div> : null,
}))
vi.mock('../components/LogHabitModal', () => ({
default: ({ open, onClose, habit, onLogDate }) => open ? (
<div data-testid="log-habit-modal">
<span>{habit?.name}</span>
<button onClick={onClose}>Close</button>
<button onClick={() => onLogDate && onLogDate(habit?.id, '2026-03-01')}>Log Date</button>
</div>
) : null,
}))
import { habitsApi } from '../api/habits'
import { tasksApi } from '../api/tasks'
const mockUser = { id: 1, username: 'testuser', email: 'test@test.com' }
const mockLogout = vi.fn()
const mockHabits = [
{ id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
{ id: 2, name: 'Read', frequency: 'weekly', target_days: [1,2,3,4,5,6,7], color: '#22c55e', icon: '📚', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
]
const mockTasks = [
{ id: 1, title: 'Buy groceries', completed: false, priority: 1, due_date: null, icon: '📋', color: '#6366f1', is_recurring: false, recurrence_type: null },
{ id: 2, title: 'Completed task', completed: true, priority: 0, due_date: null, icon: '✅', color: '#22c55e', is_recurring: false, recurrence_type: null },
]
const renderHome = () => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>
<Home />
</MemoryRouter>
</QueryClientProvider>
)
}
describe('Home page', () => {
beforeEach(() => {
vi.clearAllMocks()
useAuthStore.mockReturnValue({ user: mockUser, logout: mockLogout })
habitsApi.list.mockResolvedValue([])
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getStats.mockResolvedValue({ total_habits: 0, completion_rate: 0, today_completed: 3, active_habits: 5 })
habitsApi.getFreezes.mockResolvedValue([])
habitsApi.deleteLog.mockResolvedValue({})
tasksApi.today.mockResolvedValue([])
})
it('renders home page', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText(/Привет|Главная/)).toBeInTheDocument()
})
})
it('renders navigation', () => {
renderHome()
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
it('shows user greeting', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText(/testuser/i)).toBeInTheDocument()
})
})
it('calls logout when logout button clicked', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText(/testuser/i)).toBeInTheDocument()
})
const logoutBtn = document.querySelector('[title="Выйти"]')
if (logoutBtn) {
fireEvent.click(logoutBtn)
expect(mockLogout).toHaveBeenCalled()
}
})
it('shows progress section', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText('Прогресс на сегодня')).toBeInTheDocument()
})
})
it('shows stats when available', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText('Выполнено')).toBeInTheDocument()
expect(screen.getByText('Активных')).toBeInTheDocument()
})
})
it('shows empty tasks state', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText('Нет задач на сегодня')).toBeInTheDocument()
})
})
it('renders tasks when present', async () => {
tasksApi.today.mockResolvedValue(mockTasks)
renderHome()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
})
it('renders only active tasks in main list', async () => {
tasksApi.today.mockResolvedValue(mockTasks)
renderHome()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
})
it('shows free day message when no habits for today', async () => {
habitsApi.list.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Свободный день!')).toBeInTheDocument()
})
})
it('renders habits section', async () => {
habitsApi.list.mockResolvedValue(mockHabits)
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Привычки')).toBeInTheDocument()
})
})
it('opens create task modal when plus button clicked', async () => {
tasksApi.today.mockResolvedValue(mockTasks)
renderHome()
await waitFor(() => {
expect(screen.getByText('Задачи на сегодня')).toBeInTheDocument()
})
const plusBtns = document.querySelectorAll('button')
const plusBtn = Array.from(plusBtns).find(b => b.querySelector('svg'))
// Click the + button in tasks header
const taskHeader = screen.getByText('Задачи на сегодня')
const headerDiv = taskHeader.closest('div')
const btnsInHeader = headerDiv?.querySelectorAll('button')
if (btnsInHeader && btnsInHeader.length > 0) {
fireEvent.click(btnsInHeader[0])
await waitFor(() => {
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
})
}
})
it('opens create task modal from empty state', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText('+ Добавить задачу')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('+ Добавить задачу'))
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
})
it('closes create task modal', async () => {
renderHome()
await waitFor(() => {
expect(screen.getByText('+ Добавить задачу')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('+ Добавить задачу'))
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
fireEvent.click(screen.getByText('Close'))
expect(screen.queryByTestId('create-task-modal')).not.toBeInTheDocument()
})
it('toggles task complete', async () => {
tasksApi.complete.mockResolvedValue({ id: 1, completed: true })
tasksApi.today.mockResolvedValue([mockTasks[0]])
renderHome()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
const buttons = document.querySelectorAll('button')
const completeBtn = Array.from(buttons).find(b => b.className && b.className.includes('rounded-xl') && b.querySelector('span'))
if (completeBtn) {
fireEvent.click(completeBtn)
await waitFor(() => {
expect(tasksApi.complete).toHaveBeenCalledWith(1)
})
}
})
it('toggles uncomplete task - api setup', async () => {
tasksApi.uncomplete.mockResolvedValue({ id: 2, completed: false })
tasksApi.today.mockResolvedValue([mockTasks[0]])
renderHome()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
// Verify uncomplete api is mocked properly
expect(tasksApi.uncomplete).toBeDefined()
})
it('shows habits with daily frequency', async () => {
const dailyHabit = { id: 1, name: 'Daily Habit', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([dailyHabit])
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Daily Habit')).toBeInTheDocument()
})
})
it('shows completion message when all habits done', async () => {
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
const today = new Date().toISOString().split('T')[0]
habitsApi.getLogs.mockResolvedValue([{ id: 10, date: today }])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText(/Все привычки выполнены/)).toBeInTheDocument()
}, { timeout: 3000 })
})
it('opens log habit modal from calendar button', async () => {
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
const calendarBtn = document.querySelector('[title="Отметить за другой день"]')
if (calendarBtn) {
fireEvent.click(calendarBtn)
await waitFor(() => {
expect(screen.getByTestId('log-habit-modal')).toBeInTheDocument()
})
}
})
it('closes log habit modal', async () => {
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
const calendarBtn = document.querySelector('[title="Отметить за другой день"]')
if (calendarBtn) {
fireEvent.click(calendarBtn)
await waitFor(() => {
expect(screen.getByTestId('log-habit-modal')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Close'))
expect(screen.queryByTestId('log-habit-modal')).not.toBeInTheDocument()
}
})
it('handles habit toggle (log)', async () => {
habitsApi.log.mockResolvedValue({ id: 99, date: new Date().toISOString().split('T')[0] })
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
const habitBtns = document.querySelectorAll('button.rounded-2xl')
if (habitBtns.length > 0) {
fireEvent.click(habitBtns[0])
await waitFor(() => {
expect(habitsApi.log).toHaveBeenCalledWith(1, {})
})
}
})
it('handles habit toggle (delete log when already done)', async () => {
habitsApi.deleteLog.mockResolvedValue({})
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
const today = new Date().toISOString().split('T')[0]
habitsApi.getLogs.mockResolvedValue([{ id: 5, date: today }])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
// After logs load, find the undo button for habit
await waitFor(() => {
const undoBtns = document.querySelectorAll('[title="Отменить"]')
expect(undoBtns.length).toBeGreaterThan(0)
}, { timeout: 3000 })
})
it('renders tasks section header', async () => {
tasksApi.today.mockResolvedValue([mockTasks[0]])
renderHome()
await waitFor(() => {
expect(screen.getByText('Задачи на сегодня')).toBeInTheDocument()
})
})
it('handles log date from modal', async () => {
habitsApi.log.mockResolvedValue({ id: 99, date: '2026-03-01' })
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
habitsApi.getLogs.mockResolvedValue([])
habitsApi.getFreezes.mockResolvedValue([])
renderHome()
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
const calendarBtn = document.querySelector('[title="Отметить за другой день"]')
if (calendarBtn) {
fireEvent.click(calendarBtn)
await waitFor(() => {
expect(screen.getByTestId('log-habit-modal')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Log Date'))
await waitFor(() => {
expect(habitsApi.log).toHaveBeenCalledWith(1, { date: '2026-03-01' })
})
}
})
})
// Test helper functions directly
describe('shouldShowToday helper', () => {
it('renders frozen habit indicator when habit is frozen', async () => {
const habit = { id: 1, name: 'Frozen Habit', frequency: 'daily', color: '#6366f1', icon: '❄️', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
habitsApi.list.mockResolvedValue([habit])
habitsApi.getLogs.mockResolvedValue([])
const today = new Date().toISOString().split('T')[0]
// Create a freeze that covers today
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
habitsApi.getFreezes.mockResolvedValue([{
start_date: yesterday.toISOString().split('T')[0],
end_date: tomorrow.toISOString().split('T')[0]
}])
renderHome()
await waitFor(() => {
expect(screen.getByText(/паузе/)).toBeInTheDocument()
}, { timeout: 3000 })
})
})

View File

@@ -1,102 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import LogHabitModal from '../components/LogHabitModal'
describe('LogHabitModal', () => {
const mockHabit = { id: 1, name: 'Exercise', color: '#6366f1', icon: '🏃' }
const mockOnClose = vi.fn()
const mockOnLogDate = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('does not render when open=false', () => {
render(
<LogHabitModal
open={false}
onClose={mockOnClose}
habit={mockHabit}
completedDates={[]}
onLogDate={mockOnLogDate}
/>
)
expect(screen.queryByText('Exercise')).not.toBeInTheDocument()
})
it('renders modal when open=true', () => {
render(
<LogHabitModal
open={true}
onClose={mockOnClose}
habit={mockHabit}
completedDates={[]}
onLogDate={mockOnLogDate}
/>
)
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
it('renders calendar', () => {
render(
<LogHabitModal
open={true}
onClose={mockOnClose}
habit={mockHabit}
completedDates={[]}
onLogDate={mockOnLogDate}
/>
)
// Calendar days should be present
const dayButtons = screen.getAllByRole('button')
expect(dayButtons.length).toBeGreaterThan(1)
})
it('renders prev/next month navigation', () => {
render(
<LogHabitModal
open={true}
onClose={mockOnClose}
habit={mockHabit}
completedDates={[]}
onLogDate={mockOnLogDate}
/>
)
// Check navigation arrows
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThan(2)
})
it('calls onClose when backdrop clicked', () => {
const { container } = render(
<LogHabitModal
open={true}
onClose={mockOnClose}
habit={mockHabit}
completedDates={[]}
onLogDate={mockOnLogDate}
/>
)
// Click backdrop (first child overlay)
const backdrop = container.querySelector('.fixed.inset-0')
fireEvent.click(backdrop)
expect(mockOnClose).toHaveBeenCalled()
})
it('calls onClose when X button clicked', () => {
render(
<LogHabitModal
open={true}
onClose={mockOnClose}
habit={mockHabit}
completedDates={[]}
onLogDate={mockOnLogDate}
/>
)
// Find close button (X icon)
const closeBtn = screen.getAllByRole('button')[0]
fireEvent.click(closeBtn)
// Some button should trigger close
expect(mockOnClose).toHaveBeenCalled()
})
})

View File

@@ -1,119 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import Login from '../pages/Login'
import { useAuthStore } from '../store/auth'
vi.mock('../store/auth', () => ({
useAuthStore: vi.fn(),
}))
const mockNavigate = vi.fn()
vi.mock('react-router-dom', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
useNavigate: () => mockNavigate,
}
})
describe('Login page', () => {
const mockLogin = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
useAuthStore.mockImplementation((selector) =>
selector({ login: mockLogin })
)
})
const renderLogin = () => render(<MemoryRouter><Login /></MemoryRouter>)
it('renders login form', () => {
renderLogin()
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument()
expect(screen.getByPlaceholderText('••••••••')).toBeInTheDocument()
expect(screen.getByText('Войти')).toBeInTheDocument()
})
it('renders "С возвращением!" heading', () => {
renderLogin()
expect(screen.getByText('С возвращением!')).toBeInTheDocument()
})
it('renders forgot password link', () => {
renderLogin()
expect(screen.getByText('Забыли пароль?')).toBeInTheDocument()
})
it('renders register link', () => {
renderLogin()
expect(screen.getByText('Зарегистрируйся')).toBeInTheDocument()
})
it('submits login form successfully', async () => {
mockLogin.mockResolvedValueOnce({})
renderLogin()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'test@test.com' },
})
fireEvent.change(screen.getByPlaceholderText('••••••••'), {
target: { value: 'password123' },
})
fireEvent.click(screen.getByText('Войти'))
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith('test@test.com', 'password123')
expect(mockNavigate).toHaveBeenCalledWith('/')
})
})
it('shows error on login failure', async () => {
mockLogin.mockRejectedValueOnce({ response: { data: { error: 'Неверный пароль' } } })
renderLogin()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'test@test.com' },
})
fireEvent.change(screen.getByPlaceholderText('••••••••'), {
target: { value: 'wrongpass' },
})
fireEvent.click(screen.getByText('Войти'))
await waitFor(() => {
expect(screen.getByText('Неверный пароль')).toBeInTheDocument()
})
})
it('shows default error message on login failure', async () => {
mockLogin.mockRejectedValueOnce(new Error('Network error'))
renderLogin()
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'test@test.com' },
})
fireEvent.change(screen.getByPlaceholderText('••••••••'), {
target: { value: 'pass' },
})
fireEvent.click(screen.getByText('Войти'))
await waitFor(() => {
expect(screen.getByText('Ошибка входа')).toBeInTheDocument()
})
})
it('toggles password visibility', () => {
renderLogin()
const passwordInput = screen.getByPlaceholderText('••••••••')
expect(passwordInput.type).toBe('password')
// Find the toggle button (Eye icon button)
const toggleBtn = passwordInput.parentElement.querySelector('button[type="button"]')
fireEvent.click(toggleBtn)
expect(passwordInput.type).toBe('text')
fireEvent.click(toggleBtn)
expect(passwordInput.type).toBe('password')
})
})

View File

@@ -1,53 +0,0 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import Navigation from '../components/Navigation'
import { useAuthStore } from '../store/auth'
vi.mock('../store/auth', () => ({
useAuthStore: vi.fn(),
}))
describe('Navigation component', () => {
const renderNav = (user = null, path = '/') =>
render(
<MemoryRouter initialEntries={[path]}>
<Navigation />
</MemoryRouter>
)
beforeEach(() => {
useAuthStore.mockImplementation((selector) => selector({ user: null }))
})
it('renders navigation with main links', () => {
renderNav()
expect(screen.getByText('Главная')).toBeInTheDocument()
expect(screen.getByText('Трекер')).toBeInTheDocument()
expect(screen.getByText('Накопления')).toBeInTheDocument()
expect(screen.getByText('Настройки')).toBeInTheDocument()
})
it('renders navigation as nav element', () => {
renderNav()
expect(screen.getByRole('navigation')).toBeInTheDocument()
})
it('renders links for all nav items', () => {
renderNav()
const links = screen.getAllByRole('link')
expect(links.length).toBeGreaterThanOrEqual(4)
})
it('renders with user as owner', () => {
useAuthStore.mockImplementation((selector) => selector({ user: { id: 1 } }))
renderNav()
expect(screen.getByText('Главная')).toBeInTheDocument()
})
it('renders with non-owner user', () => {
useAuthStore.mockImplementation((selector) => selector({ user: { id: 2 } }))
renderNav()
expect(screen.getByText('Главная')).toBeInTheDocument()
})
})

View File

@@ -1,103 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import Register from '../pages/Register'
import { useAuthStore } from '../store/auth'
vi.mock('../store/auth', () => ({
useAuthStore: vi.fn(),
}))
const mockNavigate = vi.fn()
vi.mock('react-router-dom', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
useNavigate: () => mockNavigate,
}
})
describe('Register page', () => {
const mockRegister = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
useAuthStore.mockImplementation((selector) =>
selector({ register: mockRegister })
)
})
const renderRegister = () => render(<MemoryRouter><Register /></MemoryRouter>)
it('renders register form', () => {
renderRegister()
expect(screen.getByText('Создай аккаунт')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Имя')).toBeInTheDocument()
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument()
expect(screen.getByText('Создать аккаунт')).toBeInTheDocument()
})
it('renders login link', () => {
renderRegister()
expect(screen.getByText('Войти')).toBeInTheDocument()
})
it('submits registration successfully', async () => {
mockRegister.mockResolvedValueOnce({})
renderRegister()
fireEvent.change(screen.getByPlaceholderText('Имя'), {
target: { value: 'TestUser' },
})
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
target: { value: 'test@test.com' },
})
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
target: { value: 'password123' },
})
fireEvent.click(screen.getByText('Создать аккаунт'))
await waitFor(() => {
expect(mockRegister).toHaveBeenCalledWith('test@test.com', 'TestUser', 'password123')
expect(mockNavigate).toHaveBeenCalledWith('/')
})
})
it('shows error on registration failure', async () => {
mockRegister.mockRejectedValueOnce({ response: { data: { error: 'Email already used' } } })
renderRegister()
fireEvent.change(screen.getByPlaceholderText('Имя'), { target: { value: 'TestUser' } })
fireEvent.change(screen.getByPlaceholderText('your@email.com'), { target: { value: 'test@test.com' } })
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), { target: { value: 'password123' } })
fireEvent.click(screen.getByText('Создать аккаунт'))
await waitFor(() => {
expect(screen.getByText('Email already used')).toBeInTheDocument()
})
})
it('shows default error on failure', async () => {
mockRegister.mockRejectedValueOnce(new Error('Error'))
renderRegister()
fireEvent.change(screen.getByPlaceholderText('Имя'), { target: { value: 'User' } })
fireEvent.change(screen.getByPlaceholderText('your@email.com'), { target: { value: 'a@b.com' } })
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), { target: { value: 'password1' } })
fireEvent.click(screen.getByText('Создать аккаунт'))
await waitFor(() => {
expect(screen.getByText('Ошибка регистрации')).toBeInTheDocument()
})
})
it('toggles password visibility', () => {
renderRegister()
const passwordInput = screen.getByPlaceholderText('Минимум 8 символов')
expect(passwordInput.type).toBe('password')
const toggleBtn = passwordInput.parentElement.querySelector('button[type="button"]')
fireEvent.click(toggleBtn)
expect(passwordInput.type).toBe('text')
})
})

View File

@@ -1,116 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import ResetPassword from '../pages/ResetPassword'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
interceptors: {
request: { use: vi.fn() },
response: { use: vi.fn() },
},
},
}))
import api from '../api/client'
const mockNavigate = vi.fn()
vi.mock('react-router-dom', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
useNavigate: () => mockNavigate,
}
})
describe('ResetPassword page', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const renderPage = (search = '') =>
render(
<MemoryRouter initialEntries={[`/reset-password${search}`]}>
<Routes>
<Route path="/reset-password" element={<ResetPassword />} />
</Routes>
</MemoryRouter>
)
it('renders form', () => {
renderPage('?token=abc')
// Use getAllByText since "Новый пароль" appears as h1 and label
expect(screen.getAllByText('Новый пароль').length).toBeGreaterThan(0)
expect(screen.getByPlaceholderText('Минимум 8 символов')).toBeInTheDocument()
expect(screen.getByText('Сохранить пароль')).toBeInTheDocument()
})
it('shows error when no token on submit', async () => {
renderPage()
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
target: { value: 'newpassword123' },
})
fireEvent.click(screen.getByText('Сохранить пароль'))
await waitFor(() => {
expect(screen.getByText('Токен не найден')).toBeInTheDocument()
})
})
it('shows success state on successful reset', async () => {
api.post.mockResolvedValueOnce({})
renderPage('?token=valid-token')
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
target: { value: 'newpassword123' },
})
fireEvent.click(screen.getByText('Сохранить пароль'))
await waitFor(() => {
expect(screen.getByText('Пароль изменён! 🎉')).toBeInTheDocument()
}, { timeout: 3000 })
})
it('shows error on failure', async () => {
api.post.mockRejectedValueOnce({ response: { data: { error: 'Token invalid' } } })
renderPage('?token=bad-token')
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
target: { value: 'newpassword123' },
})
fireEvent.click(screen.getByText('Сохранить пароль'))
await waitFor(() => {
expect(screen.getByText('Token invalid')).toBeInTheDocument()
}, { timeout: 3000 })
})
it('toggles password visibility', () => {
renderPage('?token=abc')
const passwordInput = screen.getByPlaceholderText('Минимум 8 символов')
expect(passwordInput.type).toBe('password')
const toggleBtn = passwordInput.parentElement.querySelector('button[type="button"]')
fireEvent.click(toggleBtn)
expect(passwordInput.type).toBe('text')
})
it('calls correct API endpoint', async () => {
api.post.mockResolvedValueOnce({})
renderPage('?token=mytoken')
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
target: { value: 'mynewpassword' },
})
fireEvent.click(screen.getByText('Сохранить пароль'))
await waitFor(() => {
expect(api.post).toHaveBeenCalledWith('/auth/reset-password', {
token: 'mytoken',
new_password: 'mynewpassword',
})
}, { timeout: 3000 })
})
})

View File

@@ -1,91 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Savings from '../pages/Savings'
import { useAuthStore } from '../store/auth'
vi.mock('../store/auth', () => ({
useAuthStore: vi.fn(),
}))
vi.mock('../api/savings', () => ({
savingsApi: {
listCategories: vi.fn(),
getStats: vi.fn(),
listTransactions: vi.fn(),
createCategory: vi.fn(),
updateCategory: vi.fn(),
deleteCategory: vi.fn(),
createTransaction: vi.fn(),
deleteTransaction: vi.fn(),
getMembers: vi.fn(),
getRecurringPlans: vi.fn(),
},
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
import { savingsApi } from '../api/savings'
const mockCategories = [
{ id: 1, name: 'Квартира', target_amount: 500000, current_amount: 100000, color: '#6366f1', emoji: '🏠', is_shared: false },
]
const mockStats = {
total_balance: 100000,
categories_count: 1,
total_deposited: 150000,
total_withdrawn: 50000,
}
const renderSavings = () => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>
<Savings />
</MemoryRouter>
</QueryClientProvider>
)
}
describe('Savings page', () => {
beforeEach(() => {
vi.clearAllMocks()
useAuthStore.mockImplementation((selector) => selector({ user: { id: 1 } }))
savingsApi.listCategories.mockResolvedValue(mockCategories)
savingsApi.getStats.mockResolvedValue(mockStats)
savingsApi.listTransactions.mockResolvedValue([])
savingsApi.getMembers.mockResolvedValue([])
savingsApi.getRecurringPlans.mockResolvedValue([])
})
it('renders savings page', async () => {
renderSavings()
await waitFor(() => {
expect(screen.getByText('Накопления')).toBeInTheDocument()
})
})
it('renders navigation', () => {
renderSavings()
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
it('renders categories', async () => {
renderSavings()
await waitFor(() => {
expect(screen.getByText('Квартира')).toBeInTheDocument()
})
})
it('renders tab navigation', async () => {
renderSavings()
await waitFor(() => {
expect(screen.getByText('Обзор')).toBeInTheDocument()
})
})
})

View File

@@ -1,119 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Settings from '../pages/Settings'
import { ThemeProvider } from '../contexts/ThemeContext'
vi.mock('../api/profile', () => ({
profileApi: {
get: vi.fn(),
update: vi.fn(),
},
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
import { profileApi } from '../api/profile'
const mockProfile = {
username: 'testuser',
telegram_chat_id: 123456,
notifications_enabled: true,
timezone: 'Europe/Moscow',
morning_reminder_time: '09:00',
evening_reminder_time: '21:00',
}
const renderSettings = () => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>
<ThemeProvider>
<Settings />
</ThemeProvider>
</MemoryRouter>
</QueryClientProvider>
)
}
describe('Settings page', () => {
beforeEach(() => {
vi.clearAllMocks()
profileApi.get.mockResolvedValue(mockProfile)
})
it('shows loading state', () => {
profileApi.get.mockReturnValue(new Promise(() => {}))
renderSettings()
expect(document.querySelector('.animate-spin')).toBeInTheDocument()
})
it('renders settings page', async () => {
renderSettings()
await waitFor(() => {
expect(screen.getByText('Настройки')).toBeInTheDocument()
})
})
it('renders theme section', async () => {
renderSettings()
await waitFor(() => {
expect(screen.getByText('Оформление')).toBeInTheDocument()
})
})
it('renders profile section', async () => {
renderSettings()
await waitFor(() => {
expect(screen.getByText('Профиль')).toBeInTheDocument()
})
})
it('populates username field from profile', async () => {
renderSettings()
await waitFor(() => {
const input = screen.getByDisplayValue('testuser')
expect(input).toBeInTheDocument()
})
})
it('toggles theme', async () => {
renderSettings()
await waitFor(() => {
expect(screen.getByText(/Тёмная тема|Светлая тема/)).toBeInTheDocument()
})
const themeBtn = screen.getByText(/Тёмная тема|Светлая тема/)
fireEvent.click(themeBtn)
expect(screen.getByText(/Тёмная тема|Светлая тема/)).toBeInTheDocument()
})
it('saves settings', async () => {
profileApi.update.mockResolvedValueOnce(mockProfile)
renderSettings()
await waitFor(() => {
expect(screen.getByDisplayValue('testuser')).toBeInTheDocument()
})
fireEvent.change(screen.getByDisplayValue('testuser'), {
target: { value: 'newusername' },
})
await waitFor(() => {
// Button says "Сохранить изменения" in Settings
expect(screen.getByText(/Сохранить/)).toBeInTheDocument()
})
fireEvent.click(screen.getByText(/Сохранить/))
await waitFor(() => {
expect(profileApi.update).toHaveBeenCalled()
})
})
it('renders Telegram section', async () => {
renderSettings()
await waitFor(() => {
expect(screen.getByText('Telegram')).toBeInTheDocument()
})
})
})

View File

@@ -1,98 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Stats from '../pages/Stats'
vi.mock('recharts', () => ({
LineChart: ({ children }) => <div data-testid="line-chart">{children}</div>,
Line: () => <div />,
BarChart: ({ children }) => <div data-testid="bar-chart">{children}</div>,
Bar: () => <div />,
XAxis: () => <div />,
YAxis: () => <div />,
Tooltip: () => <div />,
ResponsiveContainer: ({ children }) => <div>{children}</div>,
Cell: () => <div />,
Area: () => <div />,
AreaChart: ({ children }) => <div data-testid="area-chart">{children}</div>,
CartesianGrid: () => <div />,
}))
vi.mock('../api/habits', () => ({
habitsApi: {
list: vi.fn(),
getStats: vi.fn(),
getLogs: vi.fn(),
},
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
import { habitsApi } from '../api/habits'
const mockStats = {
total_habits: 3,
total_logs: 45,
current_streak: 7,
best_streak: 14,
completion_rate: 82,
habits: [
{ id: 1, name: 'Exercise', completion_rate: 90, streak: 7 },
{ id: 2, name: 'Read', completion_rate: 75, streak: 3 },
],
daily_completions: [
{ date: '2026-03-01', count: 2 },
{ date: '2026-03-02', count: 3 },
],
}
const renderStats = () => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>
<Stats />
</MemoryRouter>
</QueryClientProvider>
)
}
describe('Stats page', () => {
beforeEach(() => {
vi.clearAllMocks()
habitsApi.list.mockResolvedValue([])
habitsApi.getStats.mockResolvedValue(mockStats)
habitsApi.getLogs.mockResolvedValue([])
})
it('renders stats page', async () => {
renderStats()
await waitFor(() => {
expect(screen.getByText('Статистика')).toBeInTheDocument()
})
})
it('renders navigation', () => {
renderStats()
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
it('shows habit selector', async () => {
habitsApi.list.mockResolvedValue([
{ id: 1, name: 'Exercise', color: '#6366f1', icon: '💪' },
])
renderStats()
// Open dropdown to see habit names
await waitFor(() => {
// Click the habit selector button to open dropdown
const selectorBtn = document.querySelector('button.w-full')
if (selectorBtn) fireEvent.click(selectorBtn)
})
await waitFor(() => {
expect(screen.getByText('Exercise')).toBeInTheDocument()
})
})
})

View File

@@ -1,312 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import Tasks from '../pages/Tasks'
vi.mock('../api/tasks', () => ({
tasksApi: {
list: vi.fn(),
complete: vi.fn(),
uncomplete: vi.fn(),
},
}))
vi.mock('../components/CreateTaskModal', () => ({
default: ({ open, onClose }) => open ? (
<div data-testid="create-task-modal">
<button onClick={onClose}>Close</button>
</div>
) : null,
}))
vi.mock('../components/EditTaskModal', () => ({
default: ({ open, onClose, task }) => open ? (
<div data-testid="edit-task-modal">
<span>{task?.title}</span>
<button onClick={onClose}>Close</button>
</div>
) : null,
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>,
}))
import { tasksApi } from '../api/tasks'
const mockTasks = [
{ id: 1, title: 'Buy groceries', completed: false, priority: 1, due_date: null, icon: '📋', color: '#6366f1', is_recurring: false, recurrence_type: null, description: '' },
{ id: 2, title: 'Read book', completed: false, priority: 2, due_date: '2026-03-30', icon: '📚', color: '#22c55e', is_recurring: false, recurrence_type: null, description: 'Read 30 pages' },
{ id: 3, title: 'Completed task', completed: true, priority: 3, due_date: null, icon: '✅', color: '#f59e0b', is_recurring: true, recurrence_type: 'daily', description: '' },
]
const mockTasksOverdue = [
{ id: 4, title: 'Overdue task', completed: false, priority: 0, due_date: '2020-01-01', icon: '⚠️', color: '#ef4444', is_recurring: false, recurrence_type: null, description: '' },
]
const renderTasks = (embedded = false) => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return render(
<QueryClientProvider client={qc}>
<MemoryRouter>
<Tasks embedded={embedded} />
</MemoryRouter>
</QueryClientProvider>
)
}
describe('Tasks page', () => {
beforeEach(() => {
vi.clearAllMocks()
tasksApi.list.mockResolvedValue(mockTasks.filter(t => !t.completed))
})
it('renders tasks list', async () => {
renderTasks()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
expect(screen.getByText('Read book')).toBeInTheDocument()
})
})
it('renders header when not embedded', async () => {
renderTasks(false)
await waitFor(() => {
expect(screen.getByText('Задачи')).toBeInTheDocument()
})
})
it('does not render header when embedded', async () => {
renderTasks(true)
await waitFor(() => {
expect(screen.queryByText('Задачи')).not.toBeInTheDocument()
})
})
it('renders filter buttons', async () => {
renderTasks()
await waitFor(() => {
expect(screen.getByText('Активные')).toBeInTheDocument()
expect(screen.getByText('Выполненные')).toBeInTheDocument()
expect(screen.getByText('Все')).toBeInTheDocument()
})
})
it('renders navigation when not embedded', () => {
renderTasks(false)
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
it('does not render navigation when embedded', () => {
renderTasks(true)
expect(screen.queryByTestId('navigation')).not.toBeInTheDocument()
})
it('shows empty state when no tasks', async () => {
tasksApi.list.mockResolvedValue([])
renderTasks()
await waitFor(() => {
expect(screen.getByText(/Нет активных задач/)).toBeInTheDocument()
})
})
it('shows add task button in empty state for active filter', async () => {
tasksApi.list.mockResolvedValue([])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Добавить задачу')).toBeInTheDocument()
})
})
it('opens create task modal', async () => {
renderTasks()
await waitFor(() => {
expect(screen.getByText('Задачи')).toBeInTheDocument()
})
const plusBtn = document.querySelector('button.bg-primary-500') || document.querySelector('button[class*="bg-primary"]')
if (plusBtn) {
fireEvent.click(plusBtn)
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
}
})
it('opens add task modal from empty state button', async () => {
tasksApi.list.mockResolvedValue([])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Добавить задачу')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Добавить задачу'))
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
})
it('switches to completed filter', async () => {
tasksApi.list.mockResolvedValue([mockTasks[2]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Выполненные')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Выполненные'))
await waitFor(() => {
expect(tasksApi.list).toHaveBeenCalledWith(true)
})
})
it('switches to all filter', async () => {
tasksApi.list.mockResolvedValue(mockTasks)
renderTasks()
await waitFor(() => {
expect(screen.getByText('Все')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Все'))
await waitFor(() => {
expect(tasksApi.list).toHaveBeenCalled()
})
})
it('completes a task on click', async () => {
tasksApi.complete.mockResolvedValue({ id: 1, completed: true })
tasksApi.list.mockResolvedValue([mockTasks[0]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
// Get all buttons: [0]=create(+), [1]=complete-task-btn, [2]=edit-btn
const allBtns = screen.getAllByRole('button')
if (allBtns.length > 1) {
fireEvent.click(allBtns[4])
await waitFor(() => {
expect(tasksApi.complete).toHaveBeenCalledWith(1)
}, { timeout: 3000 })
}
})
it('shows priority badge for tasks', async () => {
tasksApi.list.mockResolvedValue([mockTasks[1]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Read book')).toBeInTheDocument()
})
expect(screen.getByText('Средний')).toBeInTheDocument()
})
it('shows high priority badge', async () => {
tasksApi.list.mockResolvedValue([mockTasks[2]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Completed task')).toBeInTheDocument()
})
expect(screen.getByText('Высокий')).toBeInTheDocument()
})
it('shows due date for tasks', async () => {
tasksApi.list.mockResolvedValue([mockTasks[1]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Read book')).toBeInTheDocument()
})
expect(document.querySelector('[class*="text-gray"]')).toBeTruthy()
})
it('shows overdue indicator', async () => {
tasksApi.list.mockResolvedValue(mockTasksOverdue)
renderTasks()
await waitFor(() => {
expect(screen.getByText('Overdue task')).toBeInTheDocument()
})
expect(document.querySelector('svg')).toBeTruthy()
})
it('opens edit modal when task title clicked', async () => {
tasksApi.list.mockResolvedValue([mockTasks[0]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Buy groceries'))
await waitFor(() => {
expect(screen.getByTestId('edit-task-modal')).toBeInTheDocument()
})
})
it('closes edit modal', async () => {
tasksApi.list.mockResolvedValue([mockTasks[0]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Buy groceries'))
await waitFor(() => {
expect(screen.getByTestId('edit-task-modal')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Close'))
expect(screen.queryByTestId('edit-task-modal')).not.toBeInTheDocument()
})
it('shows recurring icon for recurring tasks', async () => {
tasksApi.list.mockResolvedValue([mockTasks[2]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Completed task')).toBeInTheDocument()
})
expect(screen.getByText('🔄')).toBeInTheDocument()
})
it('shows recurrence label', async () => {
tasksApi.list.mockResolvedValue([mockTasks[2]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Ежедневно')).toBeInTheDocument()
})
})
it('shows task description', async () => {
tasksApi.list.mockResolvedValue([mockTasks[1]])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Read 30 pages')).toBeInTheDocument()
})
})
it('uncompletes a task', async () => {
tasksApi.uncomplete.mockResolvedValue({ id: 3, completed: false })
const completedTask = { ...mockTasks[2], completed: true }
tasksApi.list.mockResolvedValue([completedTask])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Completed task')).toBeInTheDocument()
})
const undoBtn = document.querySelector('[title="Отменить"]')
if (undoBtn) {
fireEvent.click(undoBtn)
await waitFor(() => {
expect(tasksApi.uncomplete).toHaveBeenCalledWith(3)
})
}
})
it('shows empty state for completed filter', async () => {
tasksApi.list.mockResolvedValue([])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Выполненные')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Выполненные'))
await waitFor(() => {
expect(screen.getByText(/Нет выполненных задач/)).toBeInTheDocument()
})
})
it('shows empty state for all filter', async () => {
tasksApi.list.mockResolvedValue([])
renderTasks()
await waitFor(() => {
expect(screen.getByText('Все')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Все'))
await waitFor(() => {
expect(screen.getByText(/Нет задач/)).toBeInTheDocument()
})
})
})

View File

@@ -1,96 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { ThemeProvider, useTheme } from '../contexts/ThemeContext'
function ThemeConsumer() {
const { theme, toggleTheme } = useTheme()
return (
<div>
<span data-testid="theme">{theme}</span>
<button onClick={toggleTheme}>Toggle</button>
</div>
)
}
describe('ThemeContext', () => {
beforeEach(() => {
localStorage.clear()
document.documentElement.classList.remove('dark')
})
it('provides default dark theme', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
expect(screen.getByTestId('theme').textContent).toBe('dark')
})
it('reads theme from localStorage', () => {
localStorage.setItem('theme', 'light')
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
expect(screen.getByTestId('theme').textContent).toBe('light')
})
it('toggles from dark to light', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
expect(screen.getByTestId('theme').textContent).toBe('dark')
fireEvent.click(screen.getByText('Toggle'))
expect(screen.getByTestId('theme').textContent).toBe('light')
})
it('toggles from light to dark', () => {
localStorage.setItem('theme', 'light')
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
fireEvent.click(screen.getByText('Toggle'))
expect(screen.getByTestId('theme').textContent).toBe('dark')
})
it('persists theme to localStorage', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
fireEvent.click(screen.getByText('Toggle'))
expect(localStorage.getItem('theme')).toBe('light')
})
it('adds dark class to documentElement', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
expect(document.documentElement.classList.contains('dark')).toBe(true)
})
it('removes dark class when switching to light', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
)
fireEvent.click(screen.getByText('Toggle'))
expect(document.documentElement.classList.contains('dark')).toBe(false)
})
it('throws when useTheme used outside provider', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
expect(() => render(<ThemeConsumer />)).toThrow()
consoleError.mockRestore()
})
})

View File

@@ -1,66 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import TransactionList from '../components/finance/TransactionList'
vi.mock('../api/finance', () => ({
financeApi: {
listCategories: vi.fn(),
listTransactions: vi.fn(),
deleteTransaction: vi.fn(),
},
}))
import { financeApi } from '../api/finance'
const mockCategories = [
{ id: 1, name: 'Еда', type: 'expense', emoji: '🍔' },
]
const mockTransactions = [
{ id: 1, amount: 500, type: 'expense', category_id: 1, description: 'Продукты', date: '2026-03-15T00:00:00Z' },
{ id: 2, amount: 1000, type: 'income', category_id: null, description: 'Зарплата', date: '2026-03-01T00:00:00Z' },
]
describe('TransactionList', () => {
beforeEach(() => {
vi.clearAllMocks()
financeApi.listCategories.mockResolvedValue(mockCategories)
financeApi.listTransactions.mockResolvedValue(mockTransactions)
})
const renderList = () =>
render(<TransactionList onAdd={vi.fn()} month={3} year={2026} />)
it('shows loading state initially', () => {
financeApi.listCategories.mockReturnValue(new Promise(() => {}))
financeApi.listTransactions.mockReturnValue(new Promise(() => {}))
renderList()
const loadingDivs = document.querySelectorAll('.animate-pulse')
expect(loadingDivs.length).toBeGreaterThan(0)
})
it('shows transactions after loading', async () => {
renderList()
await waitFor(() => {
expect(screen.getByText('Продукты')).toBeInTheDocument()
})
})
it('shows income transaction', async () => {
renderList()
await waitFor(() => {
expect(screen.getByText('Зарплата')).toBeInTheDocument()
})
})
it('renders filter buttons', async () => {
renderList()
await waitFor(() => {
// There are two "Все" buttons (type filter and category filter), use getAllByText
const allButtons = screen.getAllByText('Все')
expect(allButtons.length).toBeGreaterThan(0)
expect(screen.getByText('Расходы')).toBeInTheDocument()
expect(screen.getByText('Доходы')).toBeInTheDocument()
})
})
})

View File

@@ -1,84 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import VerifyEmail from '../pages/VerifyEmail'
vi.mock('../api/client', () => ({
default: {
post: vi.fn(),
get: vi.fn(),
interceptors: {
request: { use: vi.fn() },
response: { use: vi.fn() },
},
},
}))
import api from '../api/client'
describe('VerifyEmail page', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const renderPage = (search = '') =>
render(
<MemoryRouter initialEntries={[`/verify-email${search}`]}>
<Routes>
<Route path="/verify-email" element={<VerifyEmail />} />
</Routes>
</MemoryRouter>
)
it('shows loading state initially (with token)', () => {
api.post.mockImplementation(() => new Promise(() => {}))
renderPage('?token=abc123')
expect(screen.getByText('Проверяем...')).toBeInTheDocument()
})
it('shows error when no token', async () => {
renderPage()
await waitFor(() => {
expect(screen.getByText('Ошибка')).toBeInTheDocument()
expect(screen.getByText('Токен не найден')).toBeInTheDocument()
})
})
it('shows success state on successful verification', async () => {
api.post.mockResolvedValueOnce({})
renderPage('?token=valid-token')
await waitFor(() => {
expect(screen.getByText('Готово! 🎉')).toBeInTheDocument()
expect(screen.getByText('Email успешно подтверждён!')).toBeInTheDocument()
})
})
it('shows login link on success', async () => {
api.post.mockResolvedValueOnce({})
renderPage('?token=valid-token')
await waitFor(() => {
expect(screen.getByText('Войти в аккаунт')).toBeInTheDocument()
})
})
it('shows error state on failed verification', async () => {
api.post.mockRejectedValueOnce({ response: { data: { error: 'Token expired' } } })
renderPage('?token=expired-token')
await waitFor(() => {
expect(screen.getByText('Ошибка')).toBeInTheDocument()
expect(screen.getByText('Token expired')).toBeInTheDocument()
})
})
it('calls verify endpoint with token', async () => {
api.post.mockResolvedValueOnce({})
renderPage('?token=mytoken')
await waitFor(() => {
expect(api.post).toHaveBeenCalledWith('/auth/verify-email', { token: 'mytoken' })
})
})
})

View File

@@ -1,26 +0,0 @@
import { render } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ThemeProvider } from '../contexts/ThemeContext'
function AllProviders({ children }) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ThemeProvider>{children}</ThemeProvider>
</BrowserRouter>
</QueryClientProvider>
)
}
export function renderWithProviders(ui, options) {
return render(ui, { wrapper: AllProviders, ...options })
}
export * from '@testing-library/react'

View File

@@ -1,5 +1,5 @@
import { NavLink } from "react-router-dom"
import { Home, BarChart3, PiggyBank, Settings } from "lucide-react"
import { Home, BarChart3, Wallet, PiggyBank, Settings } from "lucide-react"
import { useAuthStore } from "../store/auth"
import clsx from "clsx"
@@ -12,6 +12,7 @@ export default function Navigation() {
const navItems = [
{ to: "/", icon: Home, label: "Главная" },
{ to: "/tracker", icon: BarChart3, label: "Трекер" },
isOwner && { to: "/finance", icon: Wallet, label: "Финансы" },
{ to: "/savings", icon: PiggyBank, label: "Накопления" },
{ to: "/settings", icon: Settings, label: "Настройки" },
].filter(Boolean)

View File

@@ -6,6 +6,7 @@ import { format, startOfWeek, differenceInDays, parseISO, isToday, isTomorrow, i
import { ru } from 'date-fns/locale'
import { habitsApi } from '../api/habits'
import { tasksApi } from '../api/tasks'
import { financeApi } from '../api/finance'
import { useAuthStore } from '../store/auth'
import Navigation from '../components/Navigation'
import CreateTaskModal from '../components/CreateTaskModal'
@@ -97,6 +98,10 @@ export default function Home() {
})
const { data: financeSummary } = useQuery({
queryKey: ["finance-summary"],
queryFn: () => financeApi.getSummary(),
})
useEffect(() => {
if (habits.length > 0) {
loadTodayLogs()
@@ -303,6 +308,26 @@ export default function Home() {
{/* Tasks */}
{/* Finance Summary */}
{financeSummary && (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="card p-5">
<h2 className="font-semibold text-gray-900 dark:text-white mb-3">💰 Баланс</h2>
<div className="grid grid-cols-3 gap-3">
<div className="text-center">
<p className="text-lg font-bold text-green-500">+{(financeSummary.total_income || 0).toLocaleString("ru-RU")} </p>
<p className="text-xs text-gray-500">Доходы</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-red-500">-{(financeSummary.total_expense || 0).toLocaleString("ru-RU")} </p>
<p className="text-xs text-gray-500">Расходы</p>
</div>
<div className="text-center">
<p className={"text-lg font-bold " + ((financeSummary.balance || 0) >= 0 ? "text-primary-500" : "text-red-500")}>{(financeSummary.balance || 0).toLocaleString("ru-RU")} </p>
<p className="text-xs text-gray-500">Баланс</p>
</div>
</div>
</motion.div>
)}
{(activeTasks.length > 0 || !tasksLoading) && (
<div>
<div className="flex items-center justify-between mb-4">