"
7 | exit 1
8 | fi
9 |
10 | VERSION=$1
11 | RUN_MODE=false
12 |
13 | # Check for --run flag
14 | if [[ "$2" == "--run" ]]; then
15 | RUN_MODE=true
16 | fi
17 |
18 | git checkout v$VERSION
19 | # Get the directory of the script
20 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
21 |
22 | # Navigate to the project root (assuming the script is in a subdirectory like 'scripts')
23 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
24 | cd "$PROJECT_ROOT"
25 |
26 | # Create the package
27 | npm pack
28 |
29 | # Create temp directory or real directory based on RUN_MODE
30 | if [ "$RUN_MODE" = true ]; then
31 | INSTALL_DIR="$(dirname "$PROJECT_ROOT")/mock-codeclimbers/codeclimbers_install_$VERSION"
32 | mkdir -p "$INSTALL_DIR"
33 | else
34 | INSTALL_DIR=$(mktemp -d)
35 | fi
36 | cd "$INSTALL_DIR"
37 |
38 | # Create package.json with dynamic version
39 | echo "{\"dependencies\":{\"codeclimbers\":\"file:$PROJECT_ROOT/codeclimbers-$VERSION.tgz\"}}" > package.json
40 |
41 | # set environment variable
42 | if [ "$RUN_MODE" = true ]; then
43 | export CODECLIMBERS_MOCK_INSTALL=false
44 | else
45 | export NODE_ENV=development
46 | export CODECLIMBERS_MOCK_INSTALL=true
47 | fi
48 |
49 |
50 | # Install
51 | npm install
52 |
53 | # Run
54 | node node_modules/codeclimbers/bin/run.js start
55 |
56 | # Capture the exit status
57 | EXIT_STATUS=$?
58 |
59 | # Clean up
60 | if [ "$RUN_MODE" = false ]; then
61 | cd "$PROJECT_ROOT"
62 | rm -rf "$INSTALL_DIR"
63 | rm "codeclimbers-$VERSION.tgz"
64 | else
65 | echo "Installation completed in: $INSTALL_DIR"
66 | echo "The .tgz file is still in the project root directory."
67 | fi
68 |
69 | # Exit with the status from the codeclimbers execution
70 | exit $EXIT_STATUS
--------------------------------------------------------------------------------
/packages/server/src/v1/v1.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { HealthController } from '../common/infrastructure/http/controllers/health.controller'
3 | import { StartupController } from './startup/startup.controller'
4 | import { ActivitiesService } from './activities/activities.service'
5 | import { PulseController } from './activities/pulse.controller'
6 | import { WakatimeController } from './activities/wakatimeProxy.controller'
7 | import { PulseRepo } from './database/pulse.repo'
8 | import { DarwinStartupService } from './startup/darwinStartup.service'
9 | import { StartupServiceFactory } from './startup/startupService.factory'
10 | import { UnsupportedStartupService } from './startup/unsupportedStartup.service'
11 | import { WindowsStartupService } from './startup/windowsStartup.service'
12 | import { LinuxStartupService } from './startup/linuxStartup.service'
13 | import { LocalDbController } from './localdb/localDb.controller'
14 | import { LocalAuthService } from './localdb/localAuth.service'
15 | import { LocalAuthController } from './localdb/localAuth.controller'
16 | import { LocalAuthGuard } from './localdb/localAuth.guard'
17 | import { LocalDbRepo } from './localdb/localDb.repo'
18 | import { ReportService } from './activities/report.service'
19 | import { ReportController } from './activities/report.controller'
20 |
21 | @Module({
22 | imports: [],
23 | controllers: [
24 | HealthController,
25 | PulseController,
26 | WakatimeController,
27 | StartupController,
28 | LocalDbController,
29 | LocalAuthController,
30 | ReportController,
31 | ],
32 | providers: [
33 | ActivitiesService,
34 | StartupServiceFactory,
35 | PulseRepo,
36 | UnsupportedStartupService,
37 | DarwinStartupService,
38 | WindowsStartupService,
39 | LinuxStartupService,
40 | LocalAuthService,
41 | LocalAuthGuard,
42 | LocalDbRepo,
43 | ReportService,
44 | ],
45 | })
46 | export class V1Module {}
47 |
--------------------------------------------------------------------------------
/packages/app/src/components/WeeklyReports/GrowthScore.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Card, CardContent, Typography } from '@mui/material'
2 | import { WeeklyBarGraph } from './WeeklyBarGraph'
3 | import { ScoreHeader } from './ScoreHeader'
4 | import { EmptyState } from './EmptyState'
5 | import { formatMinutes } from '../../utils/time'
6 |
7 | interface Props {
8 | growthScore: CodeClimbers.WeeklyScore & {
9 | breakdown: CodeClimbers.EntityTimeOverviewDB[]
10 | }
11 | }
12 |
13 | export const GrowthScore = ({ growthScore }: Props) => {
14 | const top5Sites = growthScore.breakdown.slice(0, 5)
15 |
16 | const data = top5Sites.map((site) => ({
17 | name: site.entity.split('//')[1] || site.entity,
18 | minutes: site.minutes,
19 | }))
20 |
21 | return (
22 |
23 |
28 | theme.palette.background.paper_raised,
33 | }}
34 | >
35 |
43 | {data.length > 0 ? (
44 | <>
45 |
46 | {formatMinutes(growthScore.actual)} total
47 |
48 |
53 | `${e.id}: ${e.formattedValue} in site: ${e.indexValue}`
54 | }
55 | />
56 | >
57 | ) : (
58 |
59 | )}
60 |
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/packages/app/src/hooks/useVersionConsole.ts:
--------------------------------------------------------------------------------
1 | import { useGetLocalVersion } from '../services/health.service'
2 | import { useLatestVersion } from '../services/version.service'
3 | import { useTheme } from '@mui/material'
4 |
5 | // only show the banner one time
6 | let hasShownBanner = false
7 |
8 | export const useVersionConsoleBanner = () => {
9 | const theme = useTheme()
10 | const { data: localVersionResponse } = useGetLocalVersion()
11 | const { data: remoteVersionResponse } = useLatestVersion()
12 |
13 | if (
14 | localVersionResponse?.version &&
15 | remoteVersionResponse &&
16 | !hasShownBanner
17 | ) {
18 | console.log(
19 | `%c
20 |
21 | @@@@@@@@@@@@@@@@@@@
22 | @@@@@@@@@@@@@@@@@@@
23 | @@@ @@@
24 | @@@ @@@
25 |
26 | @@@@@@@@@@@@@@@@@@@
27 | @@@@@@@@@@@@@@@@@@@
28 | @@@ @@@
29 | @@@ @@@
30 |
31 | @@@@@@@@@@@@@@@@@@@
32 | @@@@@@@@@@@@@@@@@@@
33 | @@@ @@@
34 | @@@ @@@
35 |
36 | %cCODECLIMBERS.IO
37 | %cWelcome to CodeClimbers! We're open source and we'd love to have you join our community on Discord: https://discord.gg/zBnu8jGnHa
38 | `,
39 | `color: ${theme.palette.primary.main}; font-size: 1rem;font-weight: bold;`,
40 | `font-weight: bold; font-size: 2rem; color: ${theme.palette.primary.main}; font-family: 'Courier New', Courier, monospace;`,
41 | `color: ${theme.palette.text.primary}; font-size: 1rem;`,
42 | )
43 |
44 | console.log(
45 | '%cCLI Version: %c' + localVersionResponse?.version,
46 | `color: ${theme.palette.primary.main}`,
47 | 'color: inherit',
48 | )
49 | console.log(
50 | '%cBrowser Version: %c' + remoteVersionResponse,
51 | `color: ${theme.palette.primary.main}`,
52 | 'color: inherit',
53 | )
54 | hasShownBanner = true
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/scripts/checkDependencies.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const fs = require('fs')
3 | const path = require('path')
4 | // Read root package.json
5 | const rootPackage = JSON.parse(fs.readFileSync('package.json', 'utf8'))
6 | const rootDeps = new Set(Object.keys(rootPackage.dependencies || {}))
7 |
8 | // Get workspace directories
9 | const workspaces = rootPackage.workspaces || []
10 |
11 | let missingDeps = new Set()
12 | let unusedDeps = new Set(rootDeps)
13 |
14 | // Check each workspace
15 | workspaces.forEach((dir) => {
16 | if (fs.existsSync(path.join(dir, 'package.json'))) {
17 | const packageJson = JSON.parse(
18 | fs.readFileSync(path.join(dir, 'package.json'), 'utf8'),
19 | )
20 | const deps = new Set([...Object.keys(packageJson.dependencies || {})])
21 | // Check for missing dependencies
22 | deps.forEach((dep) => {
23 | if (!rootDeps.has(dep)) {
24 | missingDeps.add(dep)
25 | }
26 | unusedDeps.delete(dep)
27 | })
28 | unusedDeps.delete('@oclif/core')
29 | unusedDeps.delete('@oclif/plugin-warn-if-update-available')
30 | unusedDeps.delete('@codeclimbers/config')
31 | // used in commands which has no package.json
32 | unusedDeps.delete('find-process')
33 | unusedDeps.delete('server')
34 | unusedDeps.delete('open')
35 | unusedDeps.delete('picocolors')
36 | }
37 | })
38 |
39 | missingDeps.delete('@codeclimbers/server')
40 | // Report results
41 | if (missingDeps.size > 0) {
42 | console.error(
43 | 'ERROR: The following dependencies are missing from the root package.json:',
44 | )
45 | missingDeps.forEach((dep) => console.error(` - ${dep}`))
46 | process.exitCode = 1
47 | }
48 |
49 | if (unusedDeps.size > 0) {
50 | console.warn(
51 | 'WARNING: The following dependencies in root package.json are not used in any submodule:',
52 | )
53 | unusedDeps.forEach((dep) => console.warn(` - ${dep}`))
54 | }
55 |
56 | if (missingDeps.size === 0 && unusedDeps.size === 0) {
57 | console.log('All dependencies are correctly configured.')
58 | }
59 |
--------------------------------------------------------------------------------
/packages/app/src/routes/AppRoutes.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from 'react-router-dom'
2 |
3 | import { ImportPage } from '../components/ImportPage'
4 | import { ExtensionsPage } from '../components/Extensions/ExtensionsPage'
5 | import { ExtensionsLayout } from '../layouts/ExtensionsLayout'
6 | import { ContributorsPage } from '../components/ContributorsPage'
7 | import { getActiveDashboardExtensionRoutes } from '../services/extensions.service'
8 | import { InstallPage } from '../components/InstallPage'
9 | import { DashboardLayout } from '../layouts/DashboardLayout'
10 | import { ImportLayout } from '../layouts/ImportLayout'
11 | import { ReportsPage } from '../components/WeeklyReports/ReportsPage'
12 | import { HomePage } from '../components/Home/HomePage'
13 | import { GameMakersPage } from '../components/GameMakers/GameMakersPage'
14 |
15 | export const AppRoutes = () => {
16 | const extensions = getActiveDashboardExtensionRoutes()
17 | return (
18 | <>
19 |
20 | }>
21 | } />
22 | }
25 | handle={{ title: 'Extensions' }}
26 | />
27 | } />
28 | } />
29 | } />
30 |
31 | }>
32 | {extensions.map((extension) => {
33 | return (
34 | }
38 | />
39 | )
40 | })}
41 |
42 | }>
43 | } />
44 |
45 | } />
46 |
47 | >
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/packages/app/src/extensions/HourlyCategoryReport/hourlyCategoryReport.api.ts:
--------------------------------------------------------------------------------
1 | import { sqlQueryFn } from '../../api/browser/services/query.service'
2 | import { useQuery } from '@tanstack/react-query'
3 |
4 | const getQuery = (startDate: string, endDate: string, category: string) =>
5 | `WITH getMinutes (category, time) AS (
6 | SELECT category, time
7 | FROM activities_pulse
8 | WHERE activities_pulse.time BETWEEN '${startDate}' AND '${endDate}'
9 | AND ${category}
10 | GROUP BY strftime('%s', time) / 120)
11 | SELECT category, time, count() * 2 AS minutes
12 | FROM getMinutes
13 | GROUP BY strftime('%s', time) / 3600
14 | ORDER BY time;`
15 |
16 | export const useGetCodingData = (startDate: string, endDate: string) => {
17 | const query = getQuery(
18 | startDate,
19 | endDate,
20 | "(category is 'coding' or category is 'debugging')",
21 | )
22 |
23 | return useQuery({
24 | queryKey: ['hourlyCategoryReport-coding', startDate, endDate],
25 | queryFn: () => sqlQueryFn(query, 'hourlyCategoryReport-coding'),
26 | })
27 | }
28 |
29 | export const useGetBrowsingData = (startDate: string, endDate: string) => {
30 | const query = getQuery(startDate, endDate, "category is 'browsing'")
31 |
32 | return useQuery({
33 | queryKey: ['hourlyCategoryReport-browsing', startDate, endDate],
34 | queryFn: () => sqlQueryFn(query, 'hourlyCategoryReport-browsing'),
35 | })
36 | }
37 |
38 | export const useGetCommunicatingData = (startDate: string, endDate: string) => {
39 | const query = getQuery(startDate, endDate, "category is 'communicating'")
40 |
41 | return useQuery({
42 | queryKey: ['hourlyCategoryReport-communicating', startDate, endDate],
43 | queryFn: () => sqlQueryFn(query, 'hourlyCategoryReport-communicating'),
44 | })
45 | }
46 |
47 | export const useGetDesigningData = (startDate: string, endDate: string) => {
48 | const query = getQuery(startDate, endDate, "category is 'designing'")
49 |
50 | return useQuery({
51 | queryKey: ['hourlyCategoryReport-designing', startDate, endDate],
52 | queryFn: () => sqlQueryFn(query, 'hourlyCategoryReport-designing'),
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/packages/app/src/components/WeeklyReports/DeepWorkScore.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Card, CardContent, Typography } from '@mui/material'
2 | import { ScoreHeader } from './ScoreHeader'
3 | import { EmptyState } from './EmptyState'
4 | import { WeeklyLineGraph } from './WeeklyLineGraph'
5 | import { Serie } from '@nivo/line'
6 | import dayjs from 'dayjs'
7 | import { formatMinutes } from '../../utils/time'
8 | import { getColorForRating } from '../../api/browser/services/report.service'
9 |
10 | interface Props {
11 | deepWorkScore: CodeClimbers.WeeklyScore & {
12 | breakdown: CodeClimbers.DeepWorkPeriod[]
13 | }
14 | }
15 |
16 | export const DeepWorkScore = ({ deepWorkScore }: Props) => {
17 | const days = deepWorkScore.breakdown
18 |
19 | const color = getColorForRating(deepWorkScore.rating)
20 | const data: Serie[] = [
21 | {
22 | id: 'deep-work',
23 | color: color.main,
24 | data: days.map((day) => ({
25 | x: dayjs(day.startDate).format('ddd'),
26 | y: day.time / 60,
27 | })),
28 | },
29 | ]
30 |
31 | const hasNoDeepWork = data.length === 0 || data[0].data.length === 0
32 | return (
33 |
34 |
39 | theme.palette.background.paper_raised,
44 | }}
45 | >
46 |
53 | {!hasNoDeepWork ? (
54 | <>
55 |
56 | {formatMinutes(deepWorkScore.actual)} avg 5 highest days
57 |
58 |
59 | >
60 | ) : (
61 |
62 | )}
63 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/packages/app/src/components/common/UpdateBanner/UpdateBanner.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Box } from '@mui/material'
2 | import { useLatestVersion } from '../../../services/version.service'
3 | import { useBrowserStorage } from '../../../hooks/useBrowserStorage'
4 | import { CodeSnippit } from '../CodeSnippit/CodeSnippit'
5 | import { useGetLocalVersion } from '../../../services/health.service'
6 |
7 | const wasOverTwenyFourHoursAgo = (dismissedAt: number) => {
8 | const twentyFourHours = 1_000 * 60 * 60 * 24
9 | return new Date().getTime() - dismissedAt >= twentyFourHours
10 | }
11 |
12 | export const UpdateBanner = () => {
13 | const { data: localVersionResponse } = useGetLocalVersion()
14 | const [dismissedInfo, setDismissedInfo] = useBrowserStorage({
15 | key: 'update-banner-dismissed',
16 | value: {
17 | dismissed: false,
18 | dismissedAt: null as number | null,
19 | },
20 | })
21 | const wasDismissed =
22 | dismissedInfo?.dismissedAt &&
23 | !wasOverTwenyFourHoursAgo(dismissedInfo.dismissedAt)
24 |
25 | // Don't show the banner if the user has dismissed it and it was less than 24 hours ago
26 | const enableVersionPolling = !wasDismissed
27 |
28 | const remoteVersion = useLatestVersion(enableVersionPolling)
29 |
30 | if (
31 | !remoteVersion.data ||
32 | localVersionResponse?.version === remoteVersion.data ||
33 | wasDismissed ||
34 | remoteVersion.isPending ||
35 | remoteVersion.isError
36 | ) {
37 | return null
38 | }
39 | const updateCommand = `
40 | npx codeclimbers startup:disable &&
41 | npx codeclimbers@${remoteVersion.data} start
42 | `
43 | return (
44 |
45 | {
48 | setDismissedInfo({
49 | dismissed: true,
50 | dismissedAt: new Date().getTime(),
51 | })
52 | }}
53 | >
54 | An update is available! Run the following command to update
55 |
56 | Then reload the page
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/packages/app/src/components/WeeklyReports/ProjectScore.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Card, CardContent, Typography } from '@mui/material'
2 | import { WeeklyBarGraph } from './WeeklyBarGraph'
3 | import { ScoreHeader } from './ScoreHeader'
4 | import { EmptyState } from './EmptyState'
5 | import { formatMinutes } from '../../utils/time'
6 |
7 | interface Props {
8 | projectScore: CodeClimbers.WeeklyScore & {
9 | breakdown: CodeClimbers.PerProjectTimeOverviewDB[]
10 | }
11 | }
12 |
13 | export const ProjectScore = ({ projectScore }: Props) => {
14 | const knownProjects = projectScore.breakdown.filter(
15 | ({ name }) => !name.toLowerCase().includes('<<'),
16 | )
17 | const top5Projects = knownProjects.slice(0, 5)
18 |
19 | const data = top5Projects.map((project) => ({
20 | name: project.name,
21 | minutes: project.minutes,
22 | }))
23 |
24 | return (
25 |
26 |
31 | theme.palette.background.paper_raised,
36 | }}
37 | >
38 |
46 | {data.length > 0 ? (
47 | <>
48 |
49 | {formatMinutes(projectScore.actual)} total
50 |
51 |
56 | `${e.id}: ${e.formattedValue} in project: ${e.indexValue}`
57 | }
58 | />
59 | >
60 | ) : (
61 |
62 | )}
63 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/packages/app/src/api/platformServer/gameSettings.platformApi.ts:
--------------------------------------------------------------------------------
1 | import { PLATFORM_API_URL, useBetterQuery } from '..'
2 | import { gamemakerKeys } from './keys'
3 | import { platformApiRequest } from '../request'
4 | import { useMutation, useQueryClient } from '@tanstack/react-query'
5 |
6 | const useGetGameSettings = (id: string) => {
7 | const queryFn = () =>
8 | platformApiRequest({
9 | url: `${PLATFORM_API_URL}/game-maker/settings/${id}`,
10 | method: 'GET',
11 | })
12 | return useBetterQuery({
13 | queryKey: gamemakerKeys.gameSettings(id),
14 | queryFn,
15 | enabled: !!id,
16 | })
17 | }
18 |
19 | const useGetAiWeeklyReports = () => {
20 | const queryFn = () =>
21 | platformApiRequest({
22 | url: `${PLATFORM_API_URL}/game-maker/ai-weekly-reports`,
23 | method: 'GET',
24 | })
25 | return useBetterQuery<
26 | { email: string; startOfWeek: string; performanceReview: string }[],
27 | Error
28 | >({
29 | queryKey: gamemakerKeys.aiWeeklyReports,
30 | select: (data) => data.reverse().slice(0, 10),
31 | queryFn,
32 | })
33 | }
34 |
35 | type UpdateGameSettingsProps = {
36 | id: string
37 | settings: object
38 | }
39 | const useUpdateGameSettings = () => {
40 | const mutationFn = ({ id, settings }: UpdateGameSettingsProps) =>
41 | platformApiRequest({
42 | url: `${PLATFORM_API_URL}/game-maker/settings/${id}`,
43 | method: 'POST',
44 | body: settings,
45 | })
46 | return useMutation({
47 | mutationFn,
48 | })
49 | }
50 |
51 | const useRunAiWeeklyReport = () => {
52 | const queryClient = useQueryClient()
53 | const mutationFn = (body: {
54 | email: string
55 | startOfWeek: string
56 | weeklyReport: CodeClimbers.WeeklyScores
57 | }) =>
58 | platformApiRequest({
59 | url: `${PLATFORM_API_URL}/game-maker/ai-weekly-reports`,
60 | method: 'POST',
61 | body,
62 | })
63 | return useMutation({
64 | mutationFn,
65 | onSuccess: () => {
66 | queryClient.invalidateQueries({ queryKey: gamemakerKeys.aiWeeklyReports })
67 | },
68 | })
69 | }
70 |
71 | export {
72 | useGetGameSettings,
73 | useUpdateGameSettings,
74 | useRunAiWeeklyReport,
75 | useGetAiWeeklyReports,
76 | }
77 |
--------------------------------------------------------------------------------
/packages/app/src/extensions/SqlSandbox/SqlSandbox.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography, useTheme } from '@mui/material'
2 | import { useNavigate } from 'react-router-dom'
3 | import AddIcon from '@mui/icons-material/Add'
4 | import { getSqlList } from './sqlSandbox.service'
5 | import { CodeClimbersButton } from '../../components/common/CodeClimbersButton'
6 |
7 | export const SqlSandbox = () => {
8 | const navigate = useNavigate()
9 | const theme = useTheme()
10 | const handleAddClick = () => {
11 | navigate('/sql-sandbox')
12 | }
13 | return (
14 |
15 |
16 | Sql Sandbox
17 | }
22 | sx={{
23 | backgroundColor:
24 | theme.palette.mode === 'dark' ? '#EBEBEB' : '#1F2122',
25 | borderRadius: '2px',
26 | textTransform: 'none',
27 | display: 'flex',
28 | alignItems: 'center',
29 | width: 'auto',
30 | height: '32px',
31 | minWidth: 0,
32 | }}
33 | >
34 | Add
35 |
36 | {/* Add a list of saved sql queries pulled from local storage */}
37 |
38 |
39 | Saved Queries
40 | {getSqlList().map((sql) => (
41 | {
56 | navigate(`/sql-sandbox?sqlId=${sql.id}`)
57 | }}
58 | >
59 | {sql.name}
60 |
61 | ))}
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mui/material'
2 | import { Navigate } from 'react-router-dom'
3 |
4 | import { useSelectedDate } from '../../hooks/useSelectedDate'
5 | import { Time } from './Time/Time'
6 | import { useGetHealth } from '../../services/health.service'
7 | import { ExtensionsDashboard } from '../Extensions/ExtensionsDashboard'
8 | import { ExtensionsWidget } from './Extensions/ExtensionsWidget'
9 | import { Sources } from './Source/Sources'
10 | import { DateHeader } from './DateHeader'
11 | import { useSetFeaturePreference } from '../../hooks/useSetFeaturePreference'
12 | import { ErrorBoundary } from 'react-error-boundary'
13 | import { ErrorFallback } from '../ErrorFallback'
14 |
15 | const HomePage = () => {
16 | const { data: health, isPending: isHealthPending } = useGetHealth({
17 | retry: false,
18 | refetchInterval: false,
19 | })
20 | const { selectedDate, setSelectedDate } = useSelectedDate()
21 | useSetFeaturePreference()
22 |
23 | if (!health && !isHealthPending) return
24 |
25 | return (
26 |
27 |
31 |
39 |
40 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | export { HomePage }
66 |
--------------------------------------------------------------------------------
/packages/server/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core'
2 | import { AppModule } from './app.module'
3 | import { Logger, ValidationPipe } from '@nestjs/common'
4 | import { isCli, isProd } from '../utils/environment.util'
5 | import { PROCESS_NAME } from '../utils/constants'
6 | import { updateSettings } from '../utils/ini.util'
7 | import { startMigrations } from './v1/database/migrations'
8 | import { CodeClimberExceptionFilter } from './filters/codeClimbersException.filter'
9 | import { urlencoded, json } from 'express'
10 |
11 | const updatedWakatimeIniValues: Record = {
12 | api_key: 'eacb3beb-dad8-4fa1-b6ba-f89de8bf8f4a', // placeholder value
13 | api_url: 'http://localhost:14400/api/v1/wakatime',
14 | }
15 |
16 | const traceEnvironment = () => {
17 | Logger.debug(`Running as: ${process.env.NODE_ENV}`, 'main.ts')
18 | Logger.debug(`process.env: ${JSON.stringify(process.env)}`, 'main.ts')
19 | }
20 |
21 | export const bootstrap = async () => {
22 | const port = process.env.CODECLIMBERS_SERVER_PORT || 14_400
23 | const app = await NestFactory.create(AppModule, {
24 | logger: !isProd()
25 | ? ['log', 'debug', 'error', 'verbose', 'warn']
26 | : ['log', 'error', 'warn'],
27 | })
28 | traceEnvironment()
29 | app.use(json({ limit: '50mb' }))
30 | app.use(urlencoded({ extended: true, limit: '50mb' }))
31 |
32 | app.enableCors({
33 | origin: isProd()
34 | ? [
35 | 'https://codeclimbers.io',
36 | /\.codeclimbers\.io$/,
37 | 'http://localhost:5173',
38 | 'chrome-extension://fdmoefklpgbjapealpjfailnmalbgpbe',
39 | ]
40 | : [
41 | 'https://codeclimbers.io',
42 | /chrome-extension.+$/,
43 | 'http://localhost:5173',
44 | /\.codeclimbers\.io$/,
45 | /\.web\.app$/,
46 | ],
47 | credentials: true,
48 | })
49 | app.useGlobalPipes(
50 | new ValidationPipe({
51 | transform: true,
52 | transformOptions: {
53 | enableImplicitConversion: true,
54 | },
55 | }),
56 | )
57 | app.useGlobalFilters(new CodeClimberExceptionFilter())
58 | await updateSettings(updatedWakatimeIniValues)
59 | await startMigrations()
60 | await app.listen(port)
61 | process.title = PROCESS_NAME
62 | }
63 |
64 | if (!isCli()) {
65 | bootstrap()
66 | }
67 |
--------------------------------------------------------------------------------
/packages/app/src/components/WeeklyReports/WeeklyLineGraph.tsx:
--------------------------------------------------------------------------------
1 | import { LineSvgProps, ResponsiveLine } from '@nivo/line'
2 | import { useTheme } from '@mui/material'
3 | import { DatumValue } from '@nivo/core'
4 | import { getColorForRating } from '../../api/browser/services/report.service'
5 |
6 | type Props = LineSvgProps & {
7 | rating: CodeClimbers.WeeklyScoreRating
8 | }
9 |
10 | const getEvenValuesBetweenMinAndMax = (min: number, max: number): number[] => {
11 | if (min > max) {
12 | throw new Error('Minimum value cannot be greater than maximum value')
13 | }
14 | const result: number[] = []
15 | for (let i = Math.ceil(min); i <= max; i++) {
16 | if (i % 2 === 0) result.push(i)
17 | }
18 | return result
19 | }
20 | const getMinMaxFromArray = (
21 | numbers: DatumValue[],
22 | ): { min: number; max: number } => {
23 | if (numbers.length === 0) {
24 | throw new Error('Array is empty')
25 | }
26 |
27 | const min = Math.min(...(numbers as number[]))
28 | const max = Math.max(...(numbers as number[]))
29 |
30 | return { min, max }
31 | }
32 |
33 | export const WeeklyLineGraph = (props: Props) => {
34 | const theme = useTheme()
35 |
36 | const color = getColorForRating(props.rating)
37 | const yValues = props.data[0].data
38 | .map((x) => x.y)
39 | .filter((x) => x !== undefined && x !== null)
40 | const { min, max } = getMinMaxFromArray(yValues)
41 | const yTickValues = getEvenValuesBetweenMinAndMax(min, max)
42 |
43 | return (
44 | `${x}h`,
56 | tickValues: yTickValues,
57 | }}
58 | animate={false}
59 | colors={`${color.main}`}
60 | enableGridX={false}
61 | enableGridY={false}
62 | enableArea={true}
63 | areaOpacity={0.05}
64 | areaBaselineValue={0}
65 | pointBorderColor={{ from: 'serieColor' }}
66 | curve="catmullRom"
67 | theme={{
68 | axis: {
69 | ticks: {
70 | text: {
71 | fill: theme.palette.text.primary,
72 | },
73 | },
74 | },
75 | }}
76 | {...props}
77 | />
78 | )
79 | }
80 |
--------------------------------------------------------------------------------