├── .devcontainer
└── devcontainer.json
├── .dockerignore
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .prettierignore
├── .prettierrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── compose.yaml
├── eslint.config.mjs
├── package.json
├── playwright.config.ts
├── pnpm-lock.yaml
├── postcss.config.mjs
├── prettier.config.js
├── prisma
├── ERD.md
├── answers.ts
├── migrations
│ ├── 0_init
│ │ └── migration.sql
│ ├── 20240420065740_create_workbook
│ │ └── migration.sql
│ ├── 20240420094749_create_workbook_task
│ │ └── migration.sql
│ ├── 20240420112403_add_on_delete_attr_to_workbook_task
│ │ └── migration.sql
│ ├── 20240420124016_add_title_to_workbook
│ │ └── migration.sql
│ ├── 20240421100012_add_genre_to_workbook_type
│ │ └── migration.sql
│ ├── 20240428120340_add_description_to_workbook
│ │ └── migration.sql
│ ├── 20240506052005_add_priority_to_workbook_task
│ │ └── migration.sql
│ ├── 20240506122004_use_uuid_in_workbook_task
│ │ └── migration.sql
│ ├── 20240506125633_fix_foreign_keys_in_workbook_task
│ │ └── migration.sql
│ ├── 20240511061347_add_author_id_to_workbook
│ │ └── migration.sql
│ ├── 20240511063729_remove_user_id_to_workbook
│ │ └── migration.sql
│ ├── 20240705225038_add_others_to_workbook_type
│ │ └── migration.sql
│ ├── 20240806043908_add_is_replenished_to_workbook
│ │ └── migration.sql
│ ├── 20240808004755_add_editorial_url_to_workbooks
│ │ └── migration.sql
│ ├── 20240813072910_add_comment_to_workbook_task
│ │ └── migration.sql
│ ├── 20240904092455_add_others_to_contest_type
│ │ └── migration.sql
│ ├── 20240905113517_add_curriculum_to_workbook_type
│ │ └── migration.sql
│ ├── 20240908080216_add_arc_to_contest_type
│ │ └── migration.sql
│ ├── 20240922115701_add_agc_to_contest_type
│ │ └── migration.sql
│ ├── 20241002081607_add_axc_like_to_contest_type
│ │ └── migration.sql
│ ├── 20241026052940_add_aoj_courses_and_pck_to_contest_type
│ │ └── migration.sql
│ ├── 20241109004736_add_university_to_contest_type
│ │ └── migration.sql
│ ├── 20241120113542_add_aoj_jag_to_contest_type
│ │ └── migration.sql
│ ├── 20250531081213_add_url_slug_to_workbook
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma
├── seed.ts
├── submission_statuses.ts
├── tags.ts
├── task_tags.ts
├── tasks.ts
├── tasks_for_production.ts
├── users.ts
└── users_for_test.ts
├── renovate.json
├── src
├── app.css
├── app.d.ts
├── app.html
├── hooks.server.ts
├── lib
│ ├── actions
│ │ ├── handle_dropdown.ts
│ │ ├── prevent_enter_key.ts
│ │ └── update_task_result.ts
│ ├── clients
│ │ ├── aizu_online_judge.ts
│ │ ├── atcoder_problems.ts
│ │ ├── cache.ts
│ │ ├── cache_strategy.ts
│ │ ├── http_client.ts
│ │ └── index.ts
│ ├── components
│ │ ├── AtCoderUserValidationForm.svelte
│ │ ├── AuthForm.svelte
│ │ ├── ContainerWrapper.svelte
│ │ ├── ExternalLinkIcon.svelte
│ │ ├── ExternalLinkWrapper.svelte
│ │ ├── Footer.svelte
│ │ ├── FormWrapper.svelte
│ │ ├── GoogleAnalytics.svelte
│ │ ├── GradeLabel.svelte
│ │ ├── Header.svelte
│ │ ├── HeadingOne.svelte
│ │ ├── InputFieldWrapper.svelte
│ │ ├── LabelWithTooltips.svelte
│ │ ├── LabelWrapper.svelte
│ │ ├── MessageHelperWrapper.svelte
│ │ ├── Messages
│ │ │ └── ErrorMessageAndReturnButton.svelte
│ │ ├── SelectWrapper.svelte
│ │ ├── SpinnerWrapper.svelte
│ │ ├── SubmissionButton.svelte
│ │ ├── SubmissionStatus
│ │ │ ├── AcceptedCounter.svelte
│ │ │ ├── IconForUpdating.svelte
│ │ │ ├── SubmissionStatusImage.svelte
│ │ │ ├── UpdatingDropdown.svelte
│ │ │ └── UpdatingModal.svelte
│ │ ├── SubmissionStatusButton.svelte
│ │ ├── TabItemWrapper.svelte
│ │ ├── TagForm.svelte
│ │ ├── TagListForEdit.svelte
│ │ ├── TaskForm.svelte
│ │ ├── TaskGradeList.svelte
│ │ ├── TaskGrades
│ │ │ ├── GradeGuidelineTable.svelte
│ │ │ └── grade_guideline_table_data.ts
│ │ ├── TaskList.svelte
│ │ ├── TaskListForEdit.svelte
│ │ ├── TaskListSorted.svelte
│ │ ├── TaskSearchBox.svelte
│ │ ├── TaskTables
│ │ │ ├── TaskTable.svelte
│ │ │ └── TaskTableBodyCell.svelte
│ │ ├── ThermometerProgressBar.svelte
│ │ ├── ToastWrapper
│ │ │ └── ErrorMessageToast.svelte
│ │ ├── TooltipWrapper.svelte
│ │ ├── Trophies
│ │ │ └── CompletedTasks.svelte
│ │ ├── UserAccountDeletionForm.svelte
│ │ ├── UserProfile.svelte
│ │ ├── WarningMessageOnDeletingAccount.svelte
│ │ ├── WorkBook
│ │ │ ├── CommentAndHint.svelte
│ │ │ ├── WorkBookForm.svelte
│ │ │ └── WorkBookInputFields.svelte
│ │ ├── WorkBookTasks
│ │ │ └── WorkBookTasksTable.svelte
│ │ └── WorkBooks
│ │ │ ├── PublicationStatusLabel.svelte
│ │ │ ├── TitleTableBodyCell.svelte
│ │ │ ├── TitleTableHeadCell.svelte
│ │ │ ├── WorkBookBaseTable.svelte
│ │ │ └── WorkBookList.svelte
│ ├── constants
│ │ ├── external-links.ts
│ │ ├── forms.ts
│ │ ├── http-response-status-codes.ts
│ │ ├── navbar-links.ts
│ │ ├── product-info.ts
│ │ ├── tailwind-helper.ts
│ │ └── urls.ts
│ ├── example.test.ts
│ ├── index.ts
│ ├── server
│ │ ├── auth.ts
│ │ ├── database.ts
│ │ └── sample_data.ts
│ ├── services
│ │ ├── answers.ts
│ │ ├── submission_status.ts
│ │ ├── tags.ts
│ │ ├── task_results.ts
│ │ ├── task_tags.ts
│ │ ├── tasks.ts
│ │ ├── tasktagsApiService.ts
│ │ ├── users.ts
│ │ ├── validateApiService.ts
│ │ ├── workbook_tasks.ts
│ │ └── workbooks.ts
│ ├── stores
│ │ ├── active_contest_type.svelte.ts
│ │ ├── active_problem_list_tab.svelte.ts
│ │ ├── active_workbook_tab.ts
│ │ ├── error_message.ts
│ │ ├── local_storage_helper.svelte.ts
│ │ ├── replenishment_workbook.svelte.ts
│ │ └── task_grades_by_workbook_type.ts
│ ├── types
│ │ ├── answer.ts
│ │ ├── apidata.ts
│ │ ├── authorship.ts
│ │ ├── contest.ts
│ │ ├── contest_table_provider.ts
│ │ ├── floating_message.ts
│ │ ├── flowbite-svelte-wrapper.ts
│ │ ├── submission.ts
│ │ ├── tag.ts
│ │ ├── task.ts
│ │ ├── tasktag.ts
│ │ ├── url.ts
│ │ ├── user.ts
│ │ └── workbook.ts
│ ├── utils
│ │ ├── account_transfer.ts
│ │ ├── authorship.ts
│ │ ├── contest.ts
│ │ ├── contest_table_provider.ts
│ │ ├── hash.ts
│ │ ├── html.ts
│ │ ├── newline.ts
│ │ ├── task.ts
│ │ ├── time.ts
│ │ ├── url.ts
│ │ ├── workbook.ts
│ │ ├── workbook_tasks.ts
│ │ └── workbooks.ts
│ └── zod
│ │ └── schema.ts
├── routes
│ ├── (admin)
│ │ ├── account_transfer
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ │ ├── tags
│ │ │ ├── +page.server.ts
│ │ │ ├── +page.svelte
│ │ │ └── [tag_id]
│ │ │ │ ├── +page.server.ts
│ │ │ │ └── +page.svelte
│ │ └── tasks
│ │ │ ├── +page.server.ts
│ │ │ ├── +page.svelte
│ │ │ └── [task_id]
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ ├── (auth)
│ │ ├── login
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ │ ├── logout
│ │ │ └── +page.server.ts
│ │ └── signup
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ ├── +error.svelte
│ ├── +layout.server.ts
│ ├── +layout.svelte
│ ├── +page.server.ts
│ ├── +page.svelte
│ ├── about
│ │ ├── +page.svelte
│ │ └── SectionSnippets.svelte
│ ├── forgot_password
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ ├── problems
│ │ ├── +page.server.ts
│ │ ├── +page.svelte
│ │ └── [slug]
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ ├── sitemap.xml
│ │ └── +server.ts
│ ├── users
│ │ ├── [username]
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ │ └── edit
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ └── workbooks
│ │ ├── +page.server.ts
│ │ ├── +page.svelte
│ │ ├── [slug]
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ │ ├── create
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ │ └── edit
│ │ └── [slug]
│ │ ├── +page.server.ts
│ │ └── +page.svelte
└── test
│ └── lib
│ ├── clients
│ ├── aizu_online_judge.test.ts
│ ├── atcoder_problems.test.ts
│ ├── cache.test.ts
│ ├── record_requests.ts
│ └── test_data
│ │ ├── aizu_online_judge
│ │ ├── contests.json
│ │ └── tasks.json
│ │ └── atcoder_problems
│ │ ├── contests.json
│ │ └── tasks.json
│ ├── common
│ ├── test_cases
│ │ └── zip.ts
│ ├── test_helpers.test.ts
│ └── test_helpers.ts
│ ├── stores
│ ├── active_contest_type.svelte.test.ts
│ ├── active_problem_list_tab.svelte.test.ts
│ ├── active_workbook_tab.test.ts
│ ├── replenishment_workbook.test.ts
│ └── task_grades_by_workbook_type.test.ts
│ ├── utils
│ ├── account_transfer.test.ts
│ ├── authorship.test.ts
│ ├── contest.test.ts
│ ├── contest_table_provider.test.ts
│ ├── html.test.ts
│ ├── task.test.ts
│ ├── task_grade.test.ts
│ ├── test_cases
│ │ ├── account_transfer.ts
│ │ ├── contest_name_and_task_index.ts
│ │ ├── contest_name_labels.ts
│ │ ├── contest_table_provider.ts
│ │ ├── contest_type.ts
│ │ ├── task_results.ts
│ │ ├── task_table_header_name.ts
│ │ └── task_url.ts
│ ├── time.test.ts
│ ├── url.test.ts
│ ├── workbook.test.ts
│ ├── workbook_tasks.test.ts
│ └── workbooks.test.ts
│ └── zod
│ ├── account_transfer_schema.test.ts
│ ├── auth_schema.test.ts
│ └── workbook_schema.test.ts
├── static
├── ac.png
├── ac_with_editorial.png
├── completed.png
├── contest_table.png
├── favicon.png
├── grade_10Q_details.png
├── grade_11Q_4Q.png
├── ns.png
├── robots.txt
├── tle.png
├── update_answer.png
└── wa.png
├── supabase
└── migrations
│ └── 20231122085404_remote_schema.sql
├── svelte.config.js
├── tailwind.config.ts
├── tests
├── about_page.test.ts
├── global.setup.ts
├── global.teardown.ts
├── robots.test.ts
├── signin.test.ts
├── sitemap.test.ts
└── toppage.test.ts
├── tsconfig.json
├── vercel.json
└── vite.config.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | .history
2 | .pnpm-store
3 | .svelte-kit
4 | node_modules
5 | package-lock.json
6 | LICENSE
7 | CONTRIBUTING.md
8 | README.md
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [KATO-Hiro] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report / バグの報告
3 | about: Create a report to help us improve.
4 | ---
5 |
6 | ## Summary / 概要
7 |
8 | - Describe the bug. A clear and concise description of what the bug is.
9 |
10 | ## Steps to reproduce / 再現方法
11 |
12 | 1. xxx
13 | 2. yyy
14 | 3. zzz
15 |
16 | ### environments
17 |
18 | - Desktop (please complete the following information):
19 |
20 | OS: [e.g. Mac, Windows11, Linux]
21 | Browser [e.g. Chrome, Safari, Edge, Firefox]
22 | Version [e.g. 117.0.05938.44]
23 |
24 | - Smartphone (please complete the following information):
25 |
26 | Device: [e.g. iPhone15, Android13]
27 | OS: [e.g. iOS16.5, Android 13]
28 | Browser [e.g. Chrome, Safari]
29 | Version [e.g. 16.5]
30 |
31 | - Tablet (please complete the following information):
32 |
33 | OS: [e.g. Android OS, Chrome OS, Windows11, iPad OS]
34 | Browser [e.g. Chrome, Safari, Edge, Firefox]
35 | Version [e.g. 16.5]
36 |
37 | ## Expected behavior / 期待される挙動
38 |
39 | - A clear and concise description of what you expected to happen.
40 |
41 | ## Actual behavior / 実際の挙動
42 |
43 | ### Screenshots
44 |
45 | - If applicable, add screenshots to help explain your problem.
46 |
47 | ## Other notes / その他
48 |
49 | - Add any other context about the problem here.
50 | - Will you try to create a pull request?
51 | - yes / no
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request / 機能要望
3 | about: Suggest an idea for this project.
4 | ---
5 |
6 | ## Description / 説明
7 |
8 | - A clear and concise description of what you want to happen.
9 |
10 | ## Motivation / 動機
11 |
12 | - A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | ## Other notes / その他
15 |
16 | - Add any other context or screenshots about the feature request here.
17 | - Will you try to create a pull request?
18 | - yes / no
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | # Enable version updates for npm
9 | - package-ecosystem: 'npm'
10 | directory: '/'
11 | # Check the npm registry for updates every day (weekdays)
12 | schedule:
13 | interval: 'daily'
14 | versioning-strategy: increase
15 |
16 | - package-ecosystem: 'github-actions'
17 | # Workflow files stored in the
18 | # default location of `.github/workflows`
19 | directory: '/'
20 | schedule:
21 | interval: 'weekly'
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | # pnpm
133 | .pnpm-store
134 |
135 | # SvelteKit
136 | .DS_Store
137 | /build
138 | /.svelte-kit
139 | /package
140 | .env.*
141 | !.env.example
142 | vite.config.js.timestamp-*
143 | vite.config.ts.timestamp-*
144 |
145 | # VS Code
146 | .history
147 |
148 | # Vercel
149 | .vercel
150 |
151 | # Prisma
152 | prisma/.fabbrica
153 |
154 | # Directory for playwright test results
155 | test-results
156 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | lint-staged
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | resolution-mode=highest
3 | # necessary for a PNPM bug: https://github.com/pnpm/pnpm/issues/5152#issuecomment-1223449173
4 | strict-peer-dependencies=false
5 | package-import-method=clone-or-copy
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | /.pnpm-store
12 | pnpm-lock.yaml
13 | package-lock.json
14 | yarn.lock
15 | vite.config.js.timestamp-*
16 | vite.config.ts.timestamp-*
17 |
18 | prisma/ERD.md
19 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "trailingComma": "all",
6 | "printWidth": 100,
7 | "plugins": ["prettier-plugin-svelte"],
8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9 | }
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # See:
2 | # https://github.com/devcontainers/images/tree/main/src/javascript-node
3 | ARG NODE_VERSION=22
4 | FROM mcr.microsoft.com/devcontainers/javascript-node:${NODE_VERSION}
5 |
6 | WORKDIR /usr/src/app
7 | COPY . /usr/src/app
8 |
9 | RUN apt-get update \
10 | && apt-get -y install --no-install-recommends fish
11 |
12 | ENV NODE_PATH=/node_modules
13 | ENV PATH=$PATH:/node_modules/.bin
14 |
15 | RUN pnpm install --frozen-lockfile
16 |
17 | CMD ["pnpm", "dev"]
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 - 2025 AtCoder NoviSteps
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | web:
3 | build: .
4 | ports:
5 | - '5173:5173'
6 | tty: true
7 | volumes:
8 | - .:/usr/src/app:cached
9 | - ./node_modules:/usr/src/app/node_modules:cached
10 | environment:
11 | - NODE_ENV=development
12 | - DATABASE_URL=postgresql://db_user:db_password@db:5432/test_db?pgbouncer=true&connection_limit=10&connect_timeout=60&statement_timeout=60000 # Note: Local server cannot start if port is set to db:6543.
13 | - DIRECT_URL=postgresql://db_user:db_password@db:5432/test_db
14 | command: sleep infinity
15 | depends_on:
16 | - db
17 |
18 | db:
19 | image: postgres:15.1
20 | restart: unless-stopped
21 | volumes:
22 | - postgres-data:/var/lib/postgresql/data:cached
23 | environment:
24 | POSTGRES_USER: db_user
25 | POSTGRES_PASSWORD: db_password
26 | POSTGRES_DB: test_db
27 | POSTGRES_INITDB_ARGS: '--encoding=UTF8'
28 | ports:
29 | - '6543:5432'
30 |
31 | volumes:
32 | postgres-data:
33 | driver: local
34 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://eslint.org/docs/latest/use/configure/migration-guide
3 | // https://eslint.org/docs/latest/use/migrate-to-9.0.0
4 | import typescriptEslint from '@typescript-eslint/eslint-plugin';
5 | import globals from 'globals';
6 | import tsParser from '@typescript-eslint/parser';
7 | import parser from 'svelte-eslint-parser';
8 | import path from 'node:path';
9 | import { fileURLToPath } from 'node:url';
10 | import js from '@eslint/js';
11 | import { FlatCompat } from '@eslint/eslintrc';
12 |
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = path.dirname(__filename);
15 | const compat = new FlatCompat({
16 | baseDirectory: __dirname,
17 | recommendedConfig: js.configs.recommended,
18 | allConfig: js.configs.all,
19 | });
20 |
21 | export default [
22 | {
23 | ignores: [
24 | '**/.DS_Store',
25 | '**/node_modules',
26 | 'build',
27 | 'coverage',
28 | '.svelte-kit',
29 | '.vercel',
30 | 'package',
31 | '**/.env',
32 | '**/.env.*',
33 | '!**/.env.example',
34 | '.pnpm-store',
35 | '**/pnpm-lock.yaml',
36 | '**/package-lock.json',
37 | '**/yarn.lock',
38 | '**/vite.config.js.timestamp-*',
39 | '**/vite.config.ts.timestamp-*',
40 | 'prisma/.fabbrica/index.ts',
41 | ],
42 | },
43 | ...compat.extends(
44 | 'eslint:recommended',
45 | 'plugin:@typescript-eslint/recommended',
46 | 'plugin:svelte/recommended',
47 | 'prettier',
48 | ),
49 | {
50 | plugins: {
51 | '@typescript-eslint': typescriptEslint,
52 | },
53 |
54 | languageOptions: {
55 | globals: {
56 | ...globals.browser,
57 | ...globals.node,
58 | },
59 |
60 | parser: tsParser,
61 | ecmaVersion: 'latest',
62 | sourceType: 'module',
63 |
64 | parserOptions: {
65 | extraFileExtensions: ['.svelte'],
66 | },
67 | },
68 |
69 | rules: {
70 | '@typescript-eslint/ban-ts-comment': [
71 | 'error',
72 | {
73 | 'ts-expect-error': 'allow-with-description',
74 | 'ts-ignore': false,
75 | },
76 | ],
77 | },
78 | },
79 | {
80 | files: ['**/*.svelte'],
81 |
82 | languageOptions: {
83 | parser: parser,
84 | ecmaVersion: 5,
85 | sourceType: 'script',
86 |
87 | parserOptions: {
88 | parser: '@typescript-eslint/parser',
89 | },
90 | },
91 | },
92 | ];
93 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | const config: PlaywrightTestConfig = {
4 | webServer: {
5 | command: 'pnpm build && pnpm preview',
6 | port: 4173,
7 | timeout: 10000 * 1000,
8 | },
9 | testDir: 'tests',
10 | projects: [
11 | //{
12 | // name: 'setup db',
13 | // testMatch: /global\.setup\.ts/,
14 | // teardown:'cleanup db',
15 | //},
16 | //{
17 | // name: 'cleanup db',
18 | // testMatch: /global\.teardown\.ts/,
19 | //},
20 | {
21 | name: 'all',
22 | testMatch: /(.+\.)?(test|spec)\.[jt]s/,
23 | //dependencies: ['setup db'],
24 | },
25 | ],
26 | };
27 |
28 | export default config;
29 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | import tailwindcss from 'tailwindcss';
2 | import autoprefixer from 'autoprefixer';
3 |
4 | export default {
5 | plugins: [
6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
7 | tailwindcss,
8 | //But others, like autoprefixer, need to run after,
9 | autoprefixer,
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://prettier.io/docs/en/configuration.html
3 | // https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/207
4 | module.exports = {
5 | plugins: ['prettier-plugin-tailwindcss'],
6 | };
7 |
--------------------------------------------------------------------------------
/prisma/answers.ts:
--------------------------------------------------------------------------------
1 | export const answers = [
2 | {
3 | id: '1',
4 | task_id: 'abc331_a',
5 | user_id: '3',
6 | status_id: '1',
7 | },
8 | {
9 | id: '2',
10 | task_id: 'abc331_a',
11 | user_id: '2',
12 | status_id: '2',
13 | },
14 | {
15 | id: '3',
16 | task_id: 'abc331_a',
17 | user_id: '4',
18 | status_id: '1',
19 | },
20 | {
21 | id: '4',
22 | task_id: 'abc331_a',
23 | user_id: '5',
24 | status_id: '2',
25 | },
26 | {
27 | id: '5',
28 | task_id: 'abc331_b',
29 | user_id: '3',
30 | status_id: '3',
31 | },
32 | {
33 | id: '6',
34 | task_id: 'abc331_b',
35 | user_id: '2',
36 | status_id: '3',
37 | },
38 | {
39 | id: '7',
40 | task_id: 'abc331_g',
41 | user_id: '4',
42 | status_id: '3',
43 | },
44 | {
45 | id: '8',
46 | task_id: 'abc331_b',
47 | user_id: '5',
48 | status_id: '3',
49 | },
50 | {
51 | id: '9',
52 | task_id: 'abc231_a',
53 | user_id: '9',
54 | status_id: '2',
55 | },
56 | {
57 | id: '10',
58 | task_id: 'abc214_a',
59 | user_id: '9',
60 | status_id: '3',
61 | },
62 | {
63 | id: '11',
64 | task_id: 'abc202_a',
65 | user_id: '9',
66 | status_id: '1',
67 | },
68 | ];
69 |
--------------------------------------------------------------------------------
/prisma/migrations/20240420065740_create_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "WorkBookType" AS ENUM ('CREATED_BY_USER', 'TEXTBOOK', 'SOLUTION', 'THEME');
3 |
4 | -- CreateTable
5 | CREATE TABLE "workbook" (
6 | "id" SERIAL NOT NULL,
7 | "userId" TEXT NOT NULL,
8 | "isPublished" BOOLEAN NOT NULL DEFAULT false,
9 | "isOfficial" BOOLEAN NOT NULL DEFAULT false,
10 | "workBookType" "WorkBookType" NOT NULL DEFAULT 'CREATED_BY_USER',
11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12 | "updatedAt" TIMESTAMP(3) NOT NULL,
13 |
14 | CONSTRAINT "workbook_pkey" PRIMARY KEY ("id")
15 | );
16 |
17 | -- AddForeignKey
18 | ALTER TABLE "workbook" ADD CONSTRAINT "workbook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
19 |
--------------------------------------------------------------------------------
/prisma/migrations/20240420094749_create_workbook_task/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "workbooktask" (
3 | "id" TEXT NOT NULL,
4 | "workBookId" INTEGER NOT NULL,
5 | "taskId" TEXT NOT NULL,
6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
7 | "updatedAt" TIMESTAMP(3) NOT NULL,
8 |
9 | CONSTRAINT "workbooktask_pkey" PRIMARY KEY ("id")
10 | );
11 |
12 | -- CreateIndex
13 | CREATE UNIQUE INDEX "workbooktask_id_key" ON "workbooktask"("id");
14 |
15 | -- AddForeignKey
16 | ALTER TABLE "workbooktask" ADD CONSTRAINT "workbooktask_workBookId_fkey" FOREIGN KEY ("workBookId") REFERENCES "workbook"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
17 |
18 | -- AddForeignKey
19 | ALTER TABLE "workbooktask" ADD CONSTRAINT "workbooktask_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "task"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
20 |
--------------------------------------------------------------------------------
/prisma/migrations/20240420112403_add_on_delete_attr_to_workbook_task/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropForeignKey
2 | ALTER TABLE "workbooktask" DROP CONSTRAINT "workbooktask_workBookId_fkey";
3 |
4 | -- AddForeignKey
5 | ALTER TABLE "workbooktask" ADD CONSTRAINT "workbooktask_workBookId_fkey" FOREIGN KEY ("workBookId") REFERENCES "workbook"("id") ON DELETE CASCADE ON UPDATE CASCADE;
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20240420124016_add_title_to_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `title` to the `workbook` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "workbook" ADD COLUMN "title" TEXT NOT NULL;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240421100012_add_genre_to_workbook_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "WorkBookType" ADD VALUE 'GENRE';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240428120340_add_description_to_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "workbook" ADD COLUMN "description" TEXT NOT NULL DEFAULT '';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240506052005_add_priority_to_workbook_task/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `priority` to the `workbooktask` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "workbooktask" ADD COLUMN "priority" INTEGER NOT NULL;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240506122004_use_uuid_in_workbook_task/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropIndex
2 | DROP INDEX "workbooktask_id_key";
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240506125633_fix_foreign_keys_in_workbook_task/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropForeignKey
2 | ALTER TABLE "workbooktask" DROP CONSTRAINT "workbooktask_taskId_fkey";
3 |
4 | -- AddForeignKey
5 | ALTER TABLE "workbooktask" ADD CONSTRAINT "workbooktask_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "task"("task_id") ON DELETE RESTRICT ON UPDATE CASCADE;
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20240511061347_add_author_id_to_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "workbook" ADD COLUMN "authorId" TEXT;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240511063729_remove_user_id_to_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `userId` on the `workbook` table. All the data in the column will be lost.
5 | - Made the column `authorId` on table `workbook` required. This step will fail if there are existing NULL values in that column.
6 |
7 | */
8 | -- DropForeignKey
9 | ALTER TABLE "workbook" DROP CONSTRAINT "workbook_userId_fkey";
10 |
11 | -- AlterTable
12 | ALTER TABLE "workbook" DROP COLUMN "userId",
13 | ALTER COLUMN "authorId" SET NOT NULL;
14 |
15 | -- AddForeignKey
16 | ALTER TABLE "workbook" ADD CONSTRAINT "workbook_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
17 |
--------------------------------------------------------------------------------
/prisma/migrations/20240705225038_add_others_to_workbook_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "WorkBookType" ADD VALUE 'OTHERS';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240806043908_add_is_replenished_to_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "workbook" ADD COLUMN "isReplenished" BOOLEAN NOT NULL DEFAULT false;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240808004755_add_editorial_url_to_workbooks/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "workbook" ADD COLUMN "editorialUrl" TEXT NOT NULL DEFAULT '';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240813072910_add_comment_to_workbook_task/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "workbooktask" ADD COLUMN "comment" TEXT NOT NULL DEFAULT '';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240904092455_add_others_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ContestType" ADD VALUE 'OTHERS';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240905113517_add_curriculum_to_workbook_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "WorkBookType" ADD VALUE 'CURRICULUM';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240908080216_add_arc_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ContestType" ADD VALUE 'ARC';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240922115701_add_agc_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ContestType" ADD VALUE 'AGC';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20241002081607_add_axc_like_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | -- This migration adds more than one value to an enum.
3 | -- With PostgreSQL versions 11 and earlier, this is not possible
4 | -- in a single migration. This can be worked around by creating
5 | -- multiple migrations, each migration adding only one value to
6 | -- the enum.
7 |
8 |
9 | ALTER TYPE "ContestType" ADD VALUE 'ABC_LIKE';
10 | ALTER TYPE "ContestType" ADD VALUE 'ARC_LIKE';
11 | ALTER TYPE "ContestType" ADD VALUE 'AGC_LIKE';
12 |
--------------------------------------------------------------------------------
/prisma/migrations/20241026052940_add_aoj_courses_and_pck_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | -- This migration adds more than one value to an enum.
3 | -- With PostgreSQL versions 11 and earlier, this is not possible
4 | -- in a single migration. This can be worked around by creating
5 | -- multiple migrations, each migration adding only one value to
6 | -- the enum.
7 |
8 |
9 | ALTER TYPE "ContestType" ADD VALUE 'AOJ_COURSES';
10 | ALTER TYPE "ContestType" ADD VALUE 'AOJ_PCK';
11 |
--------------------------------------------------------------------------------
/prisma/migrations/20241109004736_add_university_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ContestType" ADD VALUE 'UNIVERSITY';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20241120113542_add_aoj_jag_to_contest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ContestType" ADD VALUE 'AOJ_JAG';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20250531081213_add_url_slug_to_workbook/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[urlSlug]` on the table `workbook` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "workbook" ADD COLUMN "urlSlug" TEXT;
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "workbook_urlSlug_key" ON "workbook"("urlSlug");
12 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
4 |
--------------------------------------------------------------------------------
/prisma/submission_statuses.ts:
--------------------------------------------------------------------------------
1 | // FIXME: 配色をsrc/lib/services/submission_status.tsに合わせる
2 | // 「挑戦中」と「未挑戦」に関しては、本番DBで既にwa / nsで登録されているので変更しない方がいいかも。
3 | export const submission_statuses = [
4 | {
5 | id: '1',
6 | status_name: 'ac',
7 | label_name: 'AC',
8 | image_path: 'ac.png',
9 | is_AC: true,
10 | button_color: 'green',
11 | },
12 | {
13 | id: '2',
14 | status_name: 'ac_with_editorial',
15 | label_name: '解説AC',
16 | image_path: 'ac_with_editorial.png',
17 | is_AC: true,
18 | button_color: 'blue',
19 | },
20 | {
21 | id: '3',
22 | status_name: 'wa',
23 | label_name: '挑戦中',
24 | image_path: 'wa.png',
25 | is_AC: false,
26 | button_color: 'yellow',
27 | },
28 | {
29 | id: '4',
30 | status_name: 'ns',
31 | label_name: '未挑戦',
32 | image_path: 'ns.png',
33 | is_AC: false,
34 | button_color: 'light',
35 | },
36 | ];
37 |
--------------------------------------------------------------------------------
/prisma/tasks_for_production.ts:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://kenkoooo.com/atcoder/resources/problems.json
3 | export const tasks = [];
4 |
--------------------------------------------------------------------------------
/prisma/users.ts:
--------------------------------------------------------------------------------
1 | import { Roles } from '@prisma/client';
2 |
3 | export const users = [
4 | { id: '1', name: 'admin', role: Roles.ADMIN },
5 | { id: '2', name: 'guest', role: Roles.USER },
6 | { id: '3', name: 'Alice', role: Roles.USER },
7 | { id: '4', name: 'Bob23', role: Roles.USER },
8 | { id: '5', name: 'Carol', role: Roles.USER },
9 | { id: '6', name: 'Dave4', role: Roles.USER },
10 | { id: '7', name: 'Ellen', role: Roles.USER },
11 | { id: '8', name: 'Frank', role: Roles.USER },
12 | { id: '9', name: 'hogehoge', role: Roles.USER },
13 | ];
14 |
--------------------------------------------------------------------------------
/prisma/users_for_test.ts:
--------------------------------------------------------------------------------
1 | import { Roles } from '@prisma/client';
2 |
3 | export const users = [
4 | { name: 'admin_test', role: Roles.ADMIN },
5 | { name: 'guest_test', role: Roles.USER },
6 | ];
7 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base", ":prHourlyLimitNone"],
4 | "timezone": "Asia/Tokyo",
5 | "schedule": ["before 5am"],
6 | "dependencyDashboard": true
7 | }
8 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | /* Write your global styles here, in PostCSS syntax */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | body {
7 | @apply flex flex-col items-center;
8 |
9 | font-family: Roboto, 'Noto Sans JP', sans-serif;
10 | font-optical-sizing: auto;
11 | font-weight: 400;
12 | font-style: normal;
13 | }
14 |
15 | #root {
16 | @apply w-full max-w-screen-xl;
17 | }
18 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 |
3 | import type { Roles } from '@prisma/client';
4 |
5 | // for information about these interfaces
6 | declare global {
7 | namespace App {
8 | // interface Error {}
9 | interface Locals {
10 | auth: import('lucia').AuthRequest;
11 | user: {
12 | id: string;
13 | name: string;
14 | role: Roles;
15 | atcoder_name: string;
16 | is_validated: boolean | null;
17 | };
18 | }
19 | // interface PageData {}
20 | // interface Platform {}
21 | }
22 | }
23 |
24 | // See:
25 | // https://lucia-auth.com/getting-started/sveltekit/
26 | ///
27 | declare global {
28 | namespace Lucia {
29 | type Auth = import('$lib/server/auth').Auth;
30 | type UserAttributes = {
31 | username: string;
32 | role: Roles;
33 | };
34 | // type SessionAttributes = {};
35 | }
36 | }
37 |
38 | // THIS IS IMPORTANT!!!
39 | export {};
40 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 |
17 | %sveltekit.head%
18 |
19 |
20 | %sveltekit.body%
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://lucia-auth.com/getting-started/sveltekit/
3 | // https://github.com/joysofcode/sveltekit-deploy
4 | // https://tech-blog.rakus.co.jp/entry/20230209/sveltekit
5 | import { auth } from '$lib/server/auth';
6 | import type { Handle } from '@sveltejs/kit';
7 |
8 | import * as userService from '$lib/services/users';
9 |
10 | export const handle: Handle = async ({ event, resolve }) => {
11 | event.locals.auth = auth.handleRequest(event);
12 | const session = await event.locals.auth.validate();
13 |
14 | if (!session) {
15 | return await resolve(event);
16 | }
17 |
18 | const user = await userService.getUser(session?.user.username as string);
19 |
20 | if (user) {
21 | event.locals.user = {
22 | id: user.id,
23 | name: user.username,
24 | role: user.role,
25 | atcoder_name: user.atcoder_username,
26 | is_validated: user.atcoder_validation_status,
27 | };
28 | }
29 |
30 | return await resolve(event);
31 | };
32 |
--------------------------------------------------------------------------------
/src/lib/actions/prevent_enter_key.ts:
--------------------------------------------------------------------------------
1 | // Usage:
2 | //
3 | // 1. 任意のSvelteコンポーネントにインポート
4 | //
7 |
8 | // 2. formに use:preventEnterKey を追加
9 | //
12 | export function preventEnterKey(node: HTMLElement) {
13 | node.addEventListener('keydown', handleKeyDown);
14 |
15 | return {
16 | destroy() {
17 | node.removeEventListener('keydown', handleKeyDown);
18 | },
19 | };
20 | }
21 |
22 | function handleKeyDown(event: KeyboardEvent) {
23 | if (event.key === 'Enter') {
24 | event.preventDefault();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/actions/update_task_result.ts:
--------------------------------------------------------------------------------
1 | import { fail } from '@sveltejs/kit';
2 |
3 | import * as crud from '$lib/services/task_results';
4 | import { BAD_REQUEST, UNAUTHORIZED } from '$lib/constants/http-response-status-codes';
5 |
6 | // HACK: clickを1回実行するとactionsが2回実行されてしまう。原因と修正方法が分かっていない状態。
7 | export const updateTaskResult = async (
8 | { request, locals }: { request: Request; locals: App.Locals },
9 | operationLog: string,
10 | ) => {
11 | console.log(operationLog);
12 | const response = await request.formData();
13 | const session = await locals.auth.validate();
14 |
15 | if (!session || !session.user || !session.user.userId) {
16 | return fail(UNAUTHORIZED, {
17 | message: 'ログインしていないか、もしくは、ログイン情報が不正です。',
18 | });
19 | }
20 |
21 | const userId = session.user.userId;
22 |
23 | try {
24 | const taskId = response.get('taskId') as string;
25 | const submissionStatus = response.get('submissionStatus') as string;
26 |
27 | await crud.updateTaskResult(taskId, submissionStatus, userId);
28 | } catch (error) {
29 | console.error('Failed to update task result: ', error);
30 | return fail(BAD_REQUEST);
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/lib/clients/atcoder_problems.ts:
--------------------------------------------------------------------------------
1 | import { ContestSiteApiClient } from '$lib/clients/http_client';
2 |
3 | import type { ContestsForImport } from '$lib/types/contest';
4 | import type { TasksForImport } from '$lib/types/task';
5 |
6 | import { ATCODER_PROBLEMS_API_BASE_URL } from '$lib/constants/urls';
7 |
8 | /**
9 | * The `AtCoderProblemsApiClient` class provides methods to interact with the AtCoder Problems API.
10 | * It extends the `ContestSiteApiClient` class and includes methods to fetch contests and tasks.
11 | *
12 | * @class
13 | * @extends {ContestSiteApiClient}
14 | *
15 | * @method getContests
16 | * Fetches the list of contests from the AtCoder Problems API.
17 | * @returns {Promise} A promise that resolves to the list of contests.
18 | * @throws Will throw an error if the fetch operation fails or if the response is invalid.
19 | *
20 | * @method getTasks
21 | * Fetches tasks from the AtCoder Problems API.
22 | * @returns {Promise} A promise that resolves to an array of tasks for import.
23 | * @throws Will throw an error if the fetch operation fails or if the response validation fails.
24 | */
25 | export class AtCoderProblemsApiClient extends ContestSiteApiClient {
26 | /**
27 | * Fetches the list of contests from the AtCoder Problems API.
28 | *
29 | * @returns {Promise} A promise that resolves to the list of contests.
30 | * @throws Will throw an error if the fetch operation fails or if the response is invalid.
31 | */
32 | async getContests(): Promise {
33 | try {
34 | const contests = await this.fetchApiWithConfig({
35 | baseApiUrl: ATCODER_PROBLEMS_API_BASE_URL,
36 | endpoint: 'contests.json',
37 | errorMessage: 'Failed to fetch contests from AtCoder Problems API',
38 | validateResponse: (data) => Array.isArray(data) && data.length > 0,
39 | });
40 |
41 | console.log(`Found AtCoder: ${contests.length} contests.`);
42 |
43 | return contests;
44 | } catch (error) {
45 | console.error(`Failed to fetch from AtCoder contests`, error);
46 | return [];
47 | }
48 | }
49 |
50 | /**
51 | * Fetches tasks from the AtCoder Problems API.
52 | *
53 | * @returns {Promise} A promise that resolves to an array of tasks for import.
54 | * @throws Will throw an error if the fetch operation fails or if the response validation fails.
55 | */
56 | async getTasks(): Promise {
57 | try {
58 | const tasks = await this.fetchApiWithConfig({
59 | baseApiUrl: ATCODER_PROBLEMS_API_BASE_URL,
60 | endpoint: 'problems.json',
61 | errorMessage: 'Failed to fetch tasks from AtCoder Problems API',
62 | validateResponse: (data) => Array.isArray(data) && data.length > 0,
63 | });
64 |
65 | console.log(`Found AtCoder: ${tasks.length} tasks.`);
66 |
67 | return tasks;
68 | } catch (error) {
69 | console.error(`Failed to fetch from AtCoder tasks`, error);
70 | return [];
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/clients/cache_strategy.ts:
--------------------------------------------------------------------------------
1 | import { Cache } from '$lib/clients/cache';
2 |
3 | import type { ContestsForImport } from '$lib/types/contest';
4 | import type { TasksForImport } from '$lib/types/task';
5 |
6 | /**
7 | * A strategy for caching contest and task data.
8 | * Separates the caching logic from the data fetching concerns.
9 | */
10 | export class ContestTaskCache {
11 | /**
12 | * Constructs a cache strategy with the specified contest and task caches.
13 | * @param contestCache - Cache for storing contest import data
14 | * @param taskCache - Cache for storing task import data
15 | */
16 | constructor(
17 | private readonly contestCache: Cache,
18 | private readonly taskCache: Cache,
19 | ) {}
20 |
21 | /**
22 | * Retrieves data from cache if available, otherwise fetches it using the provided function.
23 | *
24 | * @template T - The type of data being cached and returned
25 | * @param {string} key - The unique identifier for the cached data
26 | * @param {() => Promise} fetchFunction - Function that returns a Promise resolving to data of type T
27 | * @param {Cache} cache - Cache object with get and set methods for type T
28 | * @returns {Promise} - The cached data or newly fetched data
29 | *
30 | * @example
31 | * const result = await cacheInstance.getCachedOrFetch(
32 | * 'contests-123',
33 | * () => api.fetchContests(),
34 | * contestCache
35 | * );
36 | */
37 | async getCachedOrFetch(
38 | key: string,
39 | fetchFunction: () => Promise,
40 | cache: Cache,
41 | ): Promise {
42 | const cachedData = cache.get(key);
43 |
44 | if (cachedData) {
45 | console.log(`Using cached data for ${key}`);
46 | return cachedData;
47 | }
48 |
49 | console.log(`Cache miss for ${key}, fetching...`);
50 |
51 | try {
52 | const contestTasks = await fetchFunction();
53 | cache.set(key, contestTasks);
54 |
55 | return contestTasks;
56 | } catch (error) {
57 | console.error(`Failed to fetch contests and/or tasks for ${key}:`, error);
58 | return [] as unknown as T;
59 | }
60 | }
61 |
62 | /**
63 | * Gets contests from cache or fetches them.
64 | */
65 | async getCachedOrFetchContests(
66 | key: string,
67 | fetchFunction: () => Promise,
68 | ): Promise {
69 | return this.getCachedOrFetch(key, fetchFunction, this.contestCache);
70 | }
71 |
72 | /**
73 | * Gets tasks from cache or fetches them.
74 | */
75 | async getCachedOrFetchTasks(
76 | key: string,
77 | fetchFunction: () => Promise,
78 | ): Promise {
79 | return this.getCachedOrFetch(key, fetchFunction, this.taskCache);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/lib/components/ContainerWrapper.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {@render children?.()}
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ExternalLinkIcon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/ExternalLinkWrapper.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
34 |
35 | {description}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
52 |
--------------------------------------------------------------------------------
/src/lib/components/Footer.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/src/lib/components/FormWrapper.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/GoogleAnalytics.svelte:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
--------------------------------------------------------------------------------
/src/lib/components/GradeLabel.svelte:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
46 | {#if taskGrade !== TaskGrade.PENDING}
47 | {grade}
48 | {:else}
49 | {'−'}
50 | {/if}
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/lib/components/HeadingOne.svelte:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
18 |
19 |
20 | {title}
21 |
22 | {#if children}
23 | {@render children()}
24 | {/if}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/InputFieldWrapper.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
43 |
--------------------------------------------------------------------------------
/src/lib/components/LabelWithTooltips.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/LabelWrapper.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/MessageHelperWrapper.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | {#if message}
15 |
16 | {message}
17 |
18 | {/if}
19 |
--------------------------------------------------------------------------------
/src/lib/components/Messages/ErrorMessageAndReturnButton.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
18 |
19 |
20 |
25 | {errorStatus ?? ''}
26 |
27 |
28 |
{errorMessage ?? ''}
29 |
30 |
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/SelectWrapper.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/SpinnerWrapper.svelte:
--------------------------------------------------------------------------------
1 |
6 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/SubmissionButton.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/components/SubmissionStatus/AcceptedCounter.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 | {acceptedCount} / {allTaskCount}
29 |
30 |
31 | {`(${acceptedRatioPercent.toFixed(1)}%)`}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/lib/components/SubmissionStatus/IconForUpdating.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | {#if isLoggedIn}
14 |
15 |
16 | {'更新'}
17 |
18 |
19 |
20 |
21 |
22 |
23 | {/if}
24 |
--------------------------------------------------------------------------------
/src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 | {#if isLoggedIn}
29 |
30 |
31 | {'更新'}
32 |
33 |
34 |
35 | {/if}
36 |
--------------------------------------------------------------------------------
/src/lib/components/SubmissionStatusButton.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/TabItemWrapper.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 | {#if tooltipContent !== '' && titleId !== ''}
58 |
63 | {tooltipContent}
64 |
65 | {/if}
66 |
67 |
68 |
69 |
70 | handleClick(workbookType, activeProblemList)}>
71 | {#snippet titleSlot()}
72 |
73 |
74 |
75 | {title}
76 |
77 |
78 | {#if tooltipContent !== ''}
79 |
80 | {/if}
81 |
82 |
83 | {/snippet}
84 |
85 | {@render children?.()}
86 |
87 |
--------------------------------------------------------------------------------
/src/lib/components/TagListForEdit.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | タグ名
34 | 公式
35 | 公開中
36 |
37 |
38 |
39 | {#each tags as tag}
40 |
41 |
42 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
58 |
59 |
60 | {#if tag.id !== 'undefined'}
61 | 編集
62 | {:else}
63 | 未登録
64 | {/if}
65 |
66 |
67 | {/each}
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/lib/components/TaskForm.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
71 |
72 | TODO: 以下に、タグ(taskTag)を追加/編集するUIを作る予定
73 |
--------------------------------------------------------------------------------
/src/lib/components/TaskGradeList.svelte:
--------------------------------------------------------------------------------
1 |
48 |
49 | {#each taskGradeValues as taskGrade}
50 |
51 |
52 | {#if countTasks(taskGrade) && isShowTaskList(isAdmin, taskGrade)}
53 |
59 | {/if}
60 | {/each}
61 |
--------------------------------------------------------------------------------
/src/lib/components/TaskGrades/GradeGuidelineTable.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 | AtCoder Beginners Contest(通称、ABC)の配点・問題 IDと、対応するグレードの目安を示しています。
21 |
22 |
23 | また、各グレードの基準に関しては、
24 |
25 | をご覧ください。
26 |
27 |
28 |
29 |
30 |
36 |
37 | ABC の配点
38 | ABC の問題 ID
39 | 対応グレード
40 |
41 |
42 |
43 | {#each gradeGuidelineTableData as { point, task, lowerGrade, upperGrade }}
44 |
45 | {point}
46 | {task}
47 |
48 |
49 |
50 | 〜
51 |
52 |
53 |
54 | {/each}
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/lib/components/TaskGrades/grade_guideline_table_data.ts:
--------------------------------------------------------------------------------
1 | import { TaskGrade } from '$lib/types/task';
2 |
3 | type TaskGradeGuideline = {
4 | point: string;
5 | task: string;
6 | lowerGrade: TaskGrade;
7 | upperGrade: TaskGrade;
8 | };
9 |
10 | type TaskGradeGuidelines = TaskGradeGuideline[];
11 |
12 | export const gradeGuidelineTableData: TaskGradeGuidelines = [
13 | {
14 | point: '100',
15 | task: 'A',
16 | lowerGrade: TaskGrade.Q9,
17 | upperGrade: TaskGrade.Q6,
18 | },
19 | {
20 | point: '150',
21 | task: 'A、B',
22 | lowerGrade: TaskGrade.Q7,
23 | upperGrade: TaskGrade.Q5,
24 | },
25 | {
26 | point: '200',
27 | task: 'B',
28 | lowerGrade: TaskGrade.Q6,
29 | upperGrade: TaskGrade.Q4,
30 | },
31 | {
32 | point: '250',
33 | task: 'B、C',
34 | lowerGrade: TaskGrade.Q5,
35 | upperGrade: TaskGrade.Q3,
36 | },
37 | {
38 | point: '300',
39 | task: 'C',
40 | lowerGrade: TaskGrade.Q4,
41 | upperGrade: TaskGrade.Q2,
42 | },
43 | {
44 | point: '350',
45 | task: 'C、D',
46 | lowerGrade: TaskGrade.Q3,
47 | upperGrade: TaskGrade.Q1,
48 | },
49 | {
50 | point: '400',
51 | task: 'D',
52 | lowerGrade: TaskGrade.Q2,
53 | upperGrade: TaskGrade.Q1,
54 | },
55 | {
56 | point: '425',
57 | task: 'D、E',
58 | lowerGrade: TaskGrade.Q2,
59 | upperGrade: TaskGrade.Q1,
60 | },
61 | {
62 | point: '450',
63 | task: 'D、E',
64 | lowerGrade: TaskGrade.Q1,
65 | upperGrade: TaskGrade.D1,
66 | },
67 | {
68 | point: '475',
69 | task: 'E、F',
70 | lowerGrade: TaskGrade.Q1,
71 | upperGrade: TaskGrade.D2,
72 | },
73 | {
74 | point: '500',
75 | task: 'E、F',
76 | lowerGrade: TaskGrade.Q1,
77 | upperGrade: TaskGrade.D3,
78 | },
79 | {
80 | point: '525',
81 | task: 'F',
82 | lowerGrade: TaskGrade.D1,
83 | upperGrade: TaskGrade.D3,
84 | },
85 | {
86 | point: '550',
87 | task: 'F',
88 | lowerGrade: TaskGrade.D2,
89 | upperGrade: TaskGrade.D4,
90 | },
91 | {
92 | point: '575',
93 | task: 'F、G',
94 | lowerGrade: TaskGrade.D3,
95 | upperGrade: TaskGrade.D4,
96 | },
97 | {
98 | point: '600',
99 | task: 'G',
100 | lowerGrade: TaskGrade.D3,
101 | upperGrade: TaskGrade.D4,
102 | },
103 | {
104 | point: '625',
105 | task: 'G',
106 | lowerGrade: TaskGrade.D3,
107 | upperGrade: TaskGrade.D5,
108 | },
109 | {
110 | point: '650',
111 | task: 'G',
112 | lowerGrade: TaskGrade.D4,
113 | upperGrade: TaskGrade.D5,
114 | },
115 | {
116 | point: '675',
117 | task: 'G',
118 | lowerGrade: TaskGrade.D5,
119 | upperGrade: TaskGrade.D6,
120 | },
121 | ];
122 |
--------------------------------------------------------------------------------
/src/lib/components/TaskListForEdit.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | コンテストID
36 | コンテスト名
37 | 問題名
38 |
39 |
40 |
41 | {#each importContests as importContest}
42 | {#if importContest.tasks.length > 0}
43 |
44 |
45 |
50 |
51 |
52 |
57 |
58 |
59 |
60 | {#each importContest.tasks as importTask}
61 | {importTask.title}
62 | {/each}
63 |
64 |
65 |
70 |
71 |
72 | {/if}
73 | {/each}
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/lib/components/TaskListSorted.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 | 回答
29 | 問題名
30 | 出典
31 | 更新日時
32 |
33 |
34 |
35 | {#each taskResults as taskResult}
36 |
37 |
38 |
43 |
44 |
45 |
49 | {removeTaskIndexFromTitle(taskResult.title, taskResult.task_table_index)}
50 |
51 |
52 |
53 | {addContestNameToTaskIndex(taskResult.contest_id, taskResult.task_table_index)}
54 |
55 | {taskResult.updated_at.toLocaleString()}
56 |
57 | {/each}
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/lib/components/TaskTables/TaskTableBodyCell.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
26 | {@render taskGradeLabel(taskResult)}
27 |
28 |
29 | {@render taskTitleAndExternalLink(taskResult)}
30 | {@render submissionUpdaterAndLinksOfTaskDetailPage(taskResult)}
31 |
32 |
33 |
34 | {#snippet taskGradeLabel(taskResult: TaskResult)}
35 |
36 |
42 |
43 | {/snippet}
44 |
45 | {#snippet taskTitleAndExternalLink(taskResult: TaskResult)}
46 |
47 |
55 |
56 | {/snippet}
57 |
58 |
59 |
60 | {#snippet submissionUpdaterAndLinksOfTaskDetailPage(selectedTaskResult: TaskResult)}
61 |
62 |
70 |
71 |
72 |
73 | {/snippet}
74 |
--------------------------------------------------------------------------------
/src/lib/components/ToastWrapper/ErrorMessageToast.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 | {#if errorMessage !== null}
15 |
16 | {#snippet icon()}
17 |
18 | Error icon
19 | {/snippet}
20 |
21 | {errorMessage}
22 |
23 | {/if}
24 |
--------------------------------------------------------------------------------
/src/lib/components/TooltipWrapper.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 | {#if tooltipContent !== '' && titleId !== ''}
23 |
24 | {tooltipContent}
25 |
26 |
27 |
28 |
29 |
30 | {/if}
31 |
--------------------------------------------------------------------------------
/src/lib/components/Trophies/CompletedTasks.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 | {#if areAllTasksAccepted(taskResults, allTasks)}
23 |
24 | {/if}
25 |
--------------------------------------------------------------------------------
/src/lib/components/UserAccountDeletionForm.svelte:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 | {#snippet icon()}
39 |
40 | {/snippet}
41 | 警告
42 |
43 |
44 |
45 |
46 | 同意する
47 |
48 | {#if showDeleteButton}
49 |
50 |
51 |
52 | {username}さん、本当に削除しますか?
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {/if}
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/lib/components/UserProfile.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 | {#if username.length === 0}
14 | This user is not found.
15 | {:else}
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 | username:
27 | {username}
28 |
29 |
30 | AtCoder username
31 | {atcoder_username}
32 |
33 |
34 |
35 |
36 | {#if isLoggedIn}
37 |
38 | {/if}
39 | {/if}
40 |
--------------------------------------------------------------------------------
/src/lib/components/WarningMessageOnDeletingAccount.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | 以下の内容が削除されます (データの復元は困難です)
6 |
7 |
8 |
9 | アカウント
10 | 登録した回答状況
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/lib/components/WorkBook/CommentAndHint.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | {#if uniqueId !== '' && commentAndHint !== ''}
16 |
17 |
24 |
25 | {commentAndHint}
26 |
27 |
28 |
29 |
30 |
31 |
32 | {/if}
33 |
--------------------------------------------------------------------------------
/src/lib/components/WorkBooks/PublicationStatusLabel.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 | {#if !isPublished}
21 |
22 | {getPublicationStatusLabel(isPublished)}
23 |
24 | {/if}
25 |
--------------------------------------------------------------------------------
/src/lib/components/WorkBooks/TitleTableBodyCell.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/WorkBooks/TitleTableHeadCell.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
14 | タイトル
15 |
16 |
--------------------------------------------------------------------------------
/src/lib/constants/external-links.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ATCODER_BASE_URL,
3 | ATCODER_PROBLEMS_URL,
4 | AOJ_ATCODER_JOI_URL,
5 | AOJ_PCK_URL,
6 | ICPC_JAPAN_PROBLEMS_URL,
7 | GITHUB_URL,
8 | } from './urls';
9 |
10 | export const externalLinks = [
11 | { title: `AtCoder`, path: ATCODER_BASE_URL },
12 | { title: `AtCoder Problems`, path: ATCODER_PROBLEMS_URL },
13 | { title: `AOJ / AtCoder-JOI`, path: AOJ_ATCODER_JOI_URL },
14 | { title: `AOJ-PCK`, path: AOJ_PCK_URL },
15 | { title: `ICPC Japan Problems`, path: ICPC_JAPAN_PROBLEMS_URL },
16 | { title: `GitHub`, path: GITHUB_URL },
17 | ];
18 |
--------------------------------------------------------------------------------
/src/lib/constants/forms.ts:
--------------------------------------------------------------------------------
1 | export const CREATE_ACCOUNT_LABEL: string = 'アカウントを作成';
2 | export const LOGIN_LABEL: string = 'ログイン';
3 |
4 | export const GUEST_USER_NAME: string = 'guest';
5 |
6 | // Note: 2024年9月時点では、staging環境、production環境を優先
7 | // ローカル環境では書き換えて対応
8 | // TODO: ローカル環境とstaging環境、production環境では値が異なるため、切り替えられるようにする
9 | export const GUEST_USER_PASSWORD_FOR_LOCAL: string = 'Ch0kuda1';
10 | export const GUEST_USER_PASSWORD: string = 'Hell0Guest';
11 |
--------------------------------------------------------------------------------
/src/lib/constants/http-response-status-codes.ts:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
3 |
4 | // Redirection messages
5 | export const FOUND = 302;
6 | export const SEE_OTHER = 303;
7 | export const TEMPORARY_REDIRECT = 307;
8 |
9 | // Client error responses
10 | export const BAD_REQUEST = 400;
11 | export const UNAUTHORIZED = 401;
12 | export const FORBIDDEN = 403;
13 | export const NOT_FOUND = 404;
14 |
15 | // Server error responses
16 | export const INTERNAL_SERVER_ERROR = 500;
17 |
--------------------------------------------------------------------------------
/src/lib/constants/navbar-links.ts:
--------------------------------------------------------------------------------
1 | export const HOME_PAGE = `/`;
2 | export const ABOUT_PAGE = `/about`;
3 | export const SIGNUP_PAGE = `/signup`;
4 | export const LOGIN_PAGE = `/login`;
5 | export const FORGOT_PASSWORD_PAGE = `/forgot_password`;
6 | export const WORKBOOKS_PAGE = `/workbooks`;
7 | export const PROBLEMS_PAGE = `/problems`;
8 |
9 | // For Admin
10 | export const IMPORTING_PROBLEMS_PAGE = `/tasks`;
11 | export const TAGS_PAGE = `/tags`;
12 | export const ACCOUNT_TRANSFER_PAGE = `/account_transfer`;
13 |
14 | export const navbarLinks = [
15 | { title: `ホーム`, path: HOME_PAGE },
16 | { title: `問題集`, path: WORKBOOKS_PAGE },
17 | { title: `一覧表`, path: PROBLEMS_PAGE },
18 | { title: `サービスの説明`, path: ABOUT_PAGE },
19 | ];
20 |
21 | export const navbarDashboardLinks = [
22 | { title: `問題のインポート`, path: IMPORTING_PROBLEMS_PAGE },
23 | { title: `一覧表`, path: PROBLEMS_PAGE },
24 | { title: `問題集`, path: WORKBOOKS_PAGE },
25 | { title: `タグ一覧`, path: TAGS_PAGE },
26 | { title: `アカウント移行`, path: ACCOUNT_TRANSFER_PAGE },
27 | ];
28 |
--------------------------------------------------------------------------------
/src/lib/constants/product-info.ts:
--------------------------------------------------------------------------------
1 | export const PRODUCT_NAME: string = 'AtCoder NoviSteps';
2 |
3 | export const PRODUCT_URL: string = 'https://atcoder-novisteps.vercel.app/';
4 |
5 | export const PRODUCT_TEAM_URL: string = 'https://github.com/orgs/AtCoder-NoviSteps/repositories';
6 |
7 | export const PRODUCT_CATCH_PHRASE: string = '解けた喜び、伸びる楽しさ';
8 |
9 | export const PRODUCT_DESCRIPTION: string =
10 | '【非公式】 AtCoder 上の問題について、取組み状況を記録していくサイトです。各問題が細かく難易度付けされており、必要な知識を段階的に習得できます。';
11 |
12 | export const TWITTER_HANDLE_NAME: string = '@acnovisteps';
13 |
14 | export const features = [
15 | {
16 | description:
17 | '問題の回答状況を自分で記録できる: 「AC」「解説AC」「挑戦中」「未挑戦」から選べます。',
18 | },
19 | {
20 | description:
21 | '一歩先の問題に挑戦: 17段階で難易度付けされており、自分の実力に合った問題が探せます。',
22 | },
23 | {
24 | description:
25 | '問題集で得意を伸ばす・苦手を克服: 例題・類題を通して、各トピックの基礎から応用的な方法まで身につけられます。',
26 | },
27 | ];
28 |
29 | const X_BASE_URL = 'https://x.com/';
30 |
31 | export const X_OFFICIAL_ACCOUNT_URL = `${X_BASE_URL}acnovisteps`;
32 |
33 | export const members = [
34 | { name: '@けんちょん', account: `${X_BASE_URL}drken1215` },
35 | { name: '@hiro', account: `${X_BASE_URL}k_hiro1818` },
36 | { name: '@ウルズニャー', account: `${X_BASE_URL}uruzunyaa` },
37 | { name: '@nonon', account: `${X_BASE_URL}nonon_kyopro` },
38 | { name: '@Bluebery1001', account: `${X_BASE_URL}bluebery1001` },
39 | { name: '@seekworser(ぷせうど)', account: `${X_BASE_URL}pseudo_thermal` },
40 | { name: '@Satsuki / さつき先生', account: `${X_BASE_URL}Satsuki_8198` },
41 | { name: '@あべみ', account: `${X_BASE_URL}cats0830v` },
42 | { name: '@わさせき', account: `${X_BASE_URL}wasaseki` },
43 | { name: '@toshi201', account: `${X_BASE_URL}toshicon201` },
44 | ];
45 |
--------------------------------------------------------------------------------
/src/lib/constants/tailwind-helper.ts:
--------------------------------------------------------------------------------
1 | export const TOOLTIP_CLASS_BASE =
2 | 'bg-white text-gray-800 dark:text-white dark:bg-gray-700 p-2 rounded-lg shadow-lg';
3 |
--------------------------------------------------------------------------------
/src/lib/constants/urls.ts:
--------------------------------------------------------------------------------
1 | export const ATCODER_BASE_URL: string = 'https://atcoder.jp';
2 |
3 | export const ATCODER_BASE_CONTEST_URL: string = `${ATCODER_BASE_URL}/contests`;
4 |
5 | export const ATCODER_PROBLEMS_URL: string = 'https://kenkoooo.com/atcoder/#/';
6 |
7 | export const ATCODER_PROBLEMS_API_BASE_URL: string = 'https://kenkoooo.com/atcoder/resources/';
8 |
9 | export const AOJ_API_BASE_URL: string = 'https://judgeapi.u-aizu.ac.jp/';
10 |
11 | // Note:
12 | // AIZU ONLINE JUDGE (AOJ) has multiple versions: v1.0, v2.0, and v3.0.
13 | // As of late October 2024, we are using v2.0, which appears to be the main site.
14 | export const AOJ_BASE_URL: string = 'https://onlinejudge.u-aizu.ac.jp';
15 |
16 | export const AOJ_TASKS_URL: string = `${AOJ_BASE_URL}/problems`;
17 |
18 | export const AOJ_ATCODER_JOI_URL: string = 'https://joi.goodbaton.com/';
19 |
20 | export const AOJ_PCK_URL: string = 'https://pro-ktmr.github.io/aoj-pck/';
21 |
22 | export const ICPC_JAPAN_PROBLEMS_URL: string = 'https://icpc-japan-problems.irrrrr.cc/';
23 |
24 | export const GITHUB_URL: string = 'https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps';
25 |
26 | export const TASK_GRADE_CRITERIA_SHEET_URL: string =
27 | 'https://docs.google.com/spreadsheets/d/1GJbTRRBsoBaY-CXIr3dIXmxkwacV4nHOTOIMCmo__Ug/';
28 |
--------------------------------------------------------------------------------
/src/lib/example.test.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'vitest';
2 |
3 | import { getTasks } from '$lib/clients';
4 |
5 | test('call problems api', () => {
6 | getTasks();
7 | });
8 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | // place files you want to import through the `$lib` alias in this folder.
2 |
--------------------------------------------------------------------------------
/src/lib/server/auth.ts:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://lucia-auth.com/getting-started/sveltekit/
3 | // https://lucia-auth.com/database-adapters/prisma/
4 | import { lucia } from 'lucia';
5 | import { sveltekit } from 'lucia/middleware';
6 | import { dev } from '$app/environment';
7 | import { prisma } from '@lucia-auth/adapter-prisma';
8 | import client from '$lib/server/database';
9 |
10 | export const auth = lucia({
11 | env: dev ? 'DEV' : 'PROD',
12 | middleware: sveltekit(),
13 | adapter: prisma(client),
14 |
15 | // https://lucia-auth.com/reference/lucia/interfaces/#user
16 | getUserAttributes: (userData) => {
17 | return {
18 | userId: userData.id,
19 | username: userData.username,
20 | role: userData.role,
21 | };
22 | },
23 | });
24 |
25 | export type Auth = typeof auth;
26 |
--------------------------------------------------------------------------------
/src/lib/server/database.ts:
--------------------------------------------------------------------------------
1 | // See:
2 | // https://lucia-auth.com/getting-started/sveltekit/
3 | // https://www.reddit.com/r/sveltejs/comments/ozt7mk/sveltekit_with_prisma_fix/?rdt=38249
4 | import Prisma, * as PrismaScope from '@prisma/client';
5 |
6 | const PrismaClient = Prisma?.PrismaClient || PrismaScope?.PrismaClient;
7 | const client = new PrismaClient();
8 |
9 | export default client;
10 |
--------------------------------------------------------------------------------
/src/lib/server/sample_data.ts:
--------------------------------------------------------------------------------
1 | // TODO: Enable to fetch data from the database via API.
2 | export const tasks = [
3 | {
4 | contest_id: 'abc318',
5 | task_id: 'abc318_a',
6 | title: 'A - foo',
7 | grade: 'Q7',
8 | },
9 | {
10 | contest_id: 'abc231',
11 | task_id: 'abc231_a',
12 | title: 'A - Water Pressure',
13 | grade: 'Q10',
14 | },
15 | {
16 | contest_id: 'abc214',
17 | task_id: 'abc214_a',
18 | title: 'A - New Generation ABC',
19 | grade: 'Q10',
20 | },
21 | {
22 | contest_id: 'abc202',
23 | task_id: 'abc202_a',
24 | title: 'A - Three Dice',
25 | grade: 'Q9',
26 | },
27 | ];
28 |
29 | export const answers = [
30 | {
31 | task_id: 'abc231_a',
32 | user_id: 'hogehoge',
33 | submission_status: 'wa',
34 | status_id: '2',
35 | },
36 | {
37 | task_id: 'abc214_a',
38 | user_id: 'hogehoge',
39 | submission_status: 'ac',
40 | status_id: '3',
41 | },
42 | {
43 | task_id: 'abc202_a',
44 | user_id: 'hogehoge',
45 | submission_status: 'ns',
46 | status_id: '1',
47 | },
48 | ];
49 |
--------------------------------------------------------------------------------
/src/lib/services/answers.ts:
--------------------------------------------------------------------------------
1 | import { initialize } from '@quramy/prisma-fabbrica/lib/internal';
2 |
3 | import { default as prisma } from '$lib/server/database';
4 | import type { TaskAnswer } from '@prisma/client';
5 | import { sha256 } from '$lib/utils/hash';
6 |
7 | initialize({ prisma });
8 |
9 | export async function getAnswers(user_id: string) {
10 | const answers_from_db = prisma.taskAnswer.findMany({
11 | where: {
12 | user_id: user_id,
13 | },
14 | });
15 | const answersMap = new Map();
16 |
17 | (await answers_from_db).map((answer) => {
18 | answersMap.set(answer.task_id, answer);
19 | });
20 | return answersMap;
21 | }
22 |
23 | export async function getAnswersOrderedByUpdatedDesc(user_id: string): Promise {
24 | const answers_from_db = await prisma.taskAnswer.findMany({
25 | where: {
26 | user_id: { equals: user_id },
27 | },
28 | orderBy: {
29 | updated_at: 'desc',
30 | },
31 | take: -1, // Reverse the list
32 | include: {
33 | task: true,
34 | },
35 | });
36 |
37 | return answers_from_db;
38 | }
39 |
40 | export async function getAnswer(task_id: string, user_id: string) {
41 | const answers_from_db = await prisma.taskAnswer.findMany({
42 | where: {
43 | AND: [{ task_id: task_id }, { user_id: user_id }],
44 | },
45 | });
46 |
47 | if (answers_from_db.length === 0) {
48 | //TODO デフォルトはNosub?
49 | return null;
50 | }
51 | return answers_from_db[0];
52 | }
53 |
54 | export async function createAnswer(task_id: string, user_id: string, status_id: string) {
55 | const id = await sha256(task_id + user_id);
56 | const taskanswerInput: TaskAnswer = {
57 | id: id,
58 | task_id: task_id,
59 | user_id: user_id,
60 | status_id: status_id,
61 | created_at: new Date(),
62 | updated_at: new Date(),
63 | };
64 |
65 | const taskAnswer = await prisma.taskAnswer.create({
66 | data: taskanswerInput,
67 | });
68 |
69 | return taskAnswer;
70 | }
71 |
72 | export async function upsertAnswer(taskId: string, userId: string, statusId: string) {
73 | try {
74 | const id = await sha256(taskId + userId);
75 | const newAnswer = {
76 | id: id,
77 | task_id: taskId,
78 | user_id: userId,
79 | status_id: statusId,
80 | created_at: new Date(),
81 | updated_at: new Date(),
82 | };
83 |
84 | await prisma.taskAnswer.upsert({
85 | where: {
86 | task_id_user_id: { task_id: taskId, user_id: userId },
87 | },
88 | update: {
89 | status_id: statusId,
90 | },
91 | create: newAnswer, // await createAnswer(taskId, userId, statusId)とすると、一意制約違反(P2002)が発生するため
92 | });
93 | } catch (error) {
94 | console.error(
95 | `Failed to update answer with taskId ${taskId}, userId ${userId}, statusId: ${statusId}:`,
96 | error,
97 | );
98 | throw error;
99 | }
100 | }
101 |
102 | // TODO: updateAnswer()
103 | // TODO: deleteAnswer()
104 |
--------------------------------------------------------------------------------
/src/lib/services/tags.ts:
--------------------------------------------------------------------------------
1 | import { default as db } from '$lib/server/database';
2 | import type { Tag } from '$lib/types/tag';
3 |
4 | // See:
5 | // https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting
6 | export async function getTags(): Promise {
7 | const tags = await db.tag.findMany({ orderBy: { id: 'desc' } });
8 |
9 | return tags;
10 | }
11 |
12 | export async function getTag(tag_id: string): Promise {
13 | //本当はfindUniqueで取得したいがうまくいかない
14 | const tag = await db.tag.findMany({
15 | where: {
16 | id: tag_id,
17 | },
18 | });
19 | console.log(tag);
20 |
21 | return tag;
22 | }
23 |
24 | export async function getTagByName(name: string): Promise {
25 | //本当はfindUniqueで取得したいがうまくいかない
26 | const tags = await db.tag.findMany({
27 | where: {
28 | name: name,
29 | },
30 | });
31 | console.log(tags);
32 |
33 | return tags;
34 | }
35 |
36 | export async function createTag(
37 | id: string,
38 | name: string,
39 | is_official: boolean,
40 | is_published: boolean,
41 | ) {
42 | const tag = await db.tag.create({
43 | data: {
44 | id: id,
45 | name: name,
46 | is_official: is_official,
47 | is_published: is_published,
48 | },
49 | });
50 |
51 | console.log(tag);
52 | }
53 | export async function updateTag(
54 | tag_id: string,
55 | name: string,
56 | is_official: boolean,
57 | is_published: boolean,
58 | ) {
59 | const tag = await db.tag.update({
60 | where: { id: tag_id },
61 | data: {
62 | id: tag_id,
63 | name: name,
64 | is_official: is_official,
65 | is_published: is_published,
66 | },
67 | });
68 |
69 | console.log(tag);
70 | }
71 |
72 | // TODO: deleteTag()
73 |
74 | // Note:
75 | // Uncomment only when executing the following commands directly from the script.
76 | //
77 | // pnpm dlx vite-node ./prisma/tags.ts
78 | //
79 | //
80 | // async function main() {
81 | // const tags = getTags();
82 | // console.log(tags);
83 |
84 | // const tag_id = 'abc322_e';
85 | // const tag = getTag(tag_id);
86 | // console.log(tag);
87 |
88 | // // updateTag(tag_id, TagGrade.Q1);
89 | // // console.log(tag);
90 | // }
91 |
92 | // main()
93 | // .catch(async (e) => {
94 | // console.error(e);
95 | // process.exit(1);
96 | // })
97 | // .finally(async () => {
98 | // await db.$disconnect();
99 | // });
100 |
--------------------------------------------------------------------------------
/src/lib/services/task_tags.ts:
--------------------------------------------------------------------------------
1 | import { default as db } from '$lib/server/database';
2 | import type { Task } from '$lib/types/task';
3 | import type { Tag } from '$lib/types/tag';
4 | // See:
5 | // https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting
6 | export async function getTasks(tag_id: string) {
7 | const tasktags = await db.taskTag.findMany({
8 | where: {
9 | tag_id: tag_id,
10 | },
11 | include: {
12 | task: true,
13 | },
14 | });
15 |
16 | const tasks = tasktags.map((tasktag) => {
17 | return tasktag.task as Task;
18 | });
19 |
20 | return tasks;
21 | }
22 |
23 | export async function getTags(task_id: string) {
24 | const tasktags = await db.taskTag.findMany({
25 | where: {
26 | task_id: task_id,
27 | },
28 | include: {
29 | tag: true,
30 | },
31 | });
32 |
33 | const tags = tasktags.map((tasktag) => {
34 | return tasktag.tag as Tag;
35 | });
36 |
37 | return tags;
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/services/tasks.ts:
--------------------------------------------------------------------------------
1 | import { default as db } from '$lib/server/database';
2 | import { classifyContest } from '$lib/utils/contest';
3 | import type { TaskGrade } from '$lib/types/task';
4 | import type { Task } from '$lib/types/task';
5 |
6 | // See:
7 | // https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting
8 | export async function getTasks(): Promise {
9 | const tasks = await db.task.findMany({ orderBy: { task_id: 'desc' } });
10 |
11 | return tasks;
12 | }
13 |
14 | export async function getTasksByTaskId(): Promise