Compare commits

..

1 Commits

Author SHA1 Message Date
Arik Chakma
c1c83b1d13 feat: analytics 2025-05-30 22:19:56 +06:00
1435 changed files with 54983 additions and 62352 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1750679157111
"lastUpdateCheck": 1748277554631
}
}

1
.astro/types.d.ts vendored
View File

@@ -1 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -1,155 +0,0 @@
---
description: When user requests migrating old roadmap content to new folder from content-old to content folder
globs:
alwaysApply: false
---
# Content Migration Rule
## Rule Name: content-migration
## Description
This rule provides a complete process for migrating roadmap content from old structure to new structure using migration mapping files.
## When to Use
Use this rule when you need to:
- Migrate content from content-old directories to content directories
- Use a migration-mapping.json file to map topic paths to content IDs
- Populate empty content files with existing content from legacy structure
## Process
### 1. Prerequisites Check
- Verify the roadmap directory has a `migration-mapping.json` file
- Confirm `content-old/` directory exists with source content
- Confirm `content/` directory exists with target files
### 2. Migration Script Creation
Create a Node.js script with the following functionality:
```javascript
const fs = require('fs');
const path = require('path');
// Load the migration mapping
const migrationMapping = JSON.parse(fs.readFileSync('migration-mapping.json', 'utf8'));
// Function to find old content file based on topic path
function findOldContentFile(topicPath) {
const parts = topicPath.split(':');
if (parts.length === 1) {
// Top level file like "introduction"
return path.join('content-old', parts[0], 'index.md');
} else if (parts.length === 2) {
// Like "introduction:what-is-rust"
const [folder, filename] = parts;
return path.join('content-old', folder, `${filename}.md`);
} else if (parts.length === 3) {
// Like "language-basics:syntax:variables"
const [folder, subfolder, filename] = parts;
return path.join('content-old', folder, subfolder, `${filename}.md`);
}
return null;
}
// Function to find new content file based on content ID
function findNewContentFile(contentId) {
const contentDir = 'content';
const files = fs.readdirSync(contentDir);
// Find file that ends with the content ID
const matchingFile = files.find(file => file.includes(`@${contentId}.md`));
if (matchingFile) {
return path.join(contentDir, matchingFile);
}
return null;
}
// Process each mapping
console.log('Starting content migration...\n');
let migratedCount = 0;
let skippedCount = 0;
for (const [topicPath, contentId] of Object.entries(migrationMapping)) {
const oldFilePath = findOldContentFile(topicPath);
const newFilePath = findNewContentFile(contentId);
if (!oldFilePath) {
console.log(`❌ Could not determine old file path for: ${topicPath}`);
skippedCount++;
continue;
}
if (!newFilePath) {
console.log(`❌ Could not find new file for content ID: ${contentId} (topic: ${topicPath})`);
skippedCount++;
continue;
}
if (!fs.existsSync(oldFilePath)) {
console.log(`❌ Old file does not exist: ${oldFilePath} (topic: ${topicPath})`);
skippedCount++;
continue;
}
try {
// Read old content
const oldContent = fs.readFileSync(oldFilePath, 'utf8');
// Write to new file
fs.writeFileSync(newFilePath, oldContent);
console.log(`✅ Migrated: ${topicPath} -> ${path.basename(newFilePath)}`);
migratedCount++;
} catch (error) {
console.log(`❌ Error migrating ${topicPath}: ${error.message}`);
skippedCount++;
}
}
console.log(`\n📊 Migration complete:`);
console.log(` Migrated: ${migratedCount} files`);
console.log(` Skipped: ${skippedCount} files`);
console.log(` Total: ${Object.keys(migrationMapping).length} mappings`);
```
### 3. Execution Steps
1. Navigate to the roadmap directory (e.g., `src/data/roadmaps/[roadmap-name]`)
2. Create the migration script as `migrate_content.cjs`
3. Run: `node migrate_content.cjs`
4. Review the migration results
5. Clean up the temporary script file
### 4. Validation
After migration:
- Verify a few migrated files have proper content (not just titles)
- Check that the content structure matches the old content
- Ensure proper markdown formatting is preserved
## File Structure Expected
```
roadmap-directory/
├── migration-mapping.json
├── content/
│ ├── file1@contentId1.md
│ ├── file2@contentId2.md
│ └── ...
└── content-old/
├── section1/
│ ├── index.md
│ ├── topic1.md
│ └── subsection1/
│ └── subtopic1.md
└── section2/
└── ...
```
## Notes
- The migration mapping uses colons (`:`) to separate nested paths
- Content files in the new structure use the pattern `filename@contentId.md`
- The script handles 1-3 levels of nesting in the old structure
- Always create the script with `.cjs` extension to avoid ES module issues

View File

@@ -1,389 +0,0 @@
---
description: GitHub pull requests
globs:
alwaysApply: false
---
# gh cli
Work seamlessly with GitHub from the command line.
USAGE
gh <command> <subcommand> [flags]
CORE COMMANDS
auth: Authenticate gh and git with GitHub
browse: Open repositories, issues, pull requests, and more in the browser
codespace: Connect to and manage codespaces
gist: Manage gists
issue: Manage issues
org: Manage organizations
pr: Manage pull requests
project: Work with GitHub Projects.
release: Manage releases
repo: Manage repositories
GITHUB ACTIONS COMMANDS
cache: Manage GitHub Actions caches
run: View details about workflow runs
workflow: View details about GitHub Actions workflows
ALIAS COMMANDS
co: Alias for "pr checkout"
ADDITIONAL COMMANDS
alias: Create command shortcuts
api: Make an authenticated GitHub API request
attestation: Work with artifact attestations
completion: Generate shell completion scripts
config: Manage configuration for gh
extension: Manage gh extensions
gpg-key: Manage GPG keys
label: Manage labels
preview: Execute previews for gh features
ruleset: View info about repo rulesets
search: Search for repositories, issues, and pull requests
secret: Manage GitHub secrets
ssh-key: Manage SSH keys
status: Print information about relevant issues, pull requests, and notifications across repositories
variable: Manage GitHub Actions variables
HELP TOPICS
accessibility: Learn about GitHub CLI's accessibility experiences
actions: Learn about working with GitHub Actions
environment: Environment variables that can be used with gh
exit-codes: Exit codes used by gh
formatting: Formatting options for JSON data exported from gh
mintty: Information about using gh with MinTTY
reference: A comprehensive reference of all gh commands
FLAGS
--help Show help for command
--version Show gh version
EXAMPLES
$ gh issue create
$ gh repo clone cli/cli
$ gh pr checkout 321
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr
Work with GitHub pull requests.
USAGE
gh pr <command> [flags]
GENERAL COMMANDS
create: Create a pull request
list: List pull requests in a repository
status: Show status of relevant pull requests
TARGETED COMMANDS
checkout: Check out a pull request in git
checks: Show CI status for a single pull request
close: Close a pull request
comment: Add a comment to a pull request
diff: View changes in a pull request
edit: Edit a pull request
lock: Lock pull request conversation
merge: Merge a pull request
ready: Mark a pull request as ready for review
reopen: Reopen a pull request
review: Add a review to a pull request
unlock: Unlock pull request conversation
update-branch: Update a pull request branch
view: View a pull request
FLAGS
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
INHERITED FLAGS
--help Show help for command
ARGUMENTS
A pull request can be supplied as argument in any of the following formats:
- by number, e.g. "123";
- by URL, e.g. "https://github.com/OWNER/REPO/pull/123"; or
- by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1".
EXAMPLES
$ gh pr checkout 353
$ gh pr create --fill
$ gh pr view --web
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr list
List pull requests in a GitHub repository. By default, this only lists open PRs.
The search query syntax is documented here:
<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
For more information about output formatting flags, see `gh help formatting`.
USAGE
gh pr list [flags]
ALIASES
gh pr ls
FLAGS
--app string Filter by GitHub App author
-a, --assignee string Filter by assignee
-A, --author string Filter by author
-B, --base string Filter by base branch
-d, --draft Filter by draft state
-H, --head string Filter by head branch ("<owner>:<branch>" syntax not supported)
-q, --jq expression Filter JSON output using a jq expression
--json fields Output JSON with the specified fields
-l, --label strings Filter by label
-L, --limit int Maximum number of items to fetch (default 30)
-S, --search query Search pull requests with query
-s, --state string Filter by state: {open|closed|merged|all} (default "open")
-t, --template string Format JSON output using a Go template; see "gh help formatting"
-w, --web List pull requests in the web browser
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
JSON FIELDS
additions, assignees, author, autoMergeRequest, baseRefName, baseRefOid, body,
changedFiles, closed, closedAt, closingIssuesReferences, comments, commits,
createdAt, deletions, files, fullDatabaseId, headRefName, headRefOid,
headRepository, headRepositoryOwner, id, isCrossRepository, isDraft, labels,
latestReviews, maintainerCanModify, mergeCommit, mergeStateStatus, mergeable,
mergedAt, mergedBy, milestone, number, potentialMergeCommit, projectCards,
projectItems, reactionGroups, reviewDecision, reviewRequests, reviews, state,
statusCheckRollup, title, updatedAt, url
EXAMPLES
# List PRs authored by you
$ gh pr list --author "@me"
# List PRs with a specific head branch name
$ gh pr list --head "typo"
# List only PRs with all of the given labels
$ gh pr list --label bug --label "priority 1"
# Filter PRs using search syntax
$ gh pr list --search "status:success review:required"
# Find a PR that introduced a given commit
$ gh pr list --search "<SHA>" --state merged
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr diff
View changes in a pull request.
Without an argument, the pull request that belongs to the current branch
is selected.
With `--web` flag, open the pull request diff in a web browser instead.
USAGE
gh pr diff [<number> | <url> | <branch>] [flags]
FLAGS
--color string Use color in diff output: {always|never|auto} (default "auto")
--name-only Display only names of changed files
--patch Display diff in patch format
-w, --web Open the pull request diff in the browser
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr merge
Merge a pull request on GitHub.
Without an argument, the pull request that belongs to the current branch
is selected.
When targeting a branch that requires a merge queue, no merge strategy is required.
If required checks have not yet passed, auto-merge will be enabled.
If required checks have passed, the pull request will be added to the merge queue.
To bypass a merge queue and merge directly, pass the `--admin` flag.
USAGE
gh pr merge [<number> | <url> | <branch>] [flags]
FLAGS
--admin Use administrator privileges to merge a pull request that does not meet requirements
-A, --author-email text Email text for merge commit author
--auto Automatically merge only after necessary requirements are met
-b, --body text Body text for the merge commit
-F, --body-file file Read body text from file (use "-" to read from standard input)
-d, --delete-branch Delete the local and remote branch after merge
--disable-auto Disable auto-merge for this pull request
--match-head-commit SHA Commit SHA that the pull request head must match to allow merge
-m, --merge Merge the commits with the base branch
-r, --rebase Rebase the commits onto the base branch
-s, --squash Squash the commits into one commit and merge it into the base branch
-t, --subject text Subject text for the merge commit
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr review
Add a review to a pull request.
Without an argument, the pull request that belongs to the current branch is reviewed.
USAGE
gh pr review [<number> | <url> | <branch>] [flags]
FLAGS
-a, --approve Approve pull request
-b, --body string Specify the body of a review
-F, --body-file file Read body text from file (use "-" to read from standard input)
-c, --comment Comment on a pull request
-r, --request-changes Request changes on a pull request
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
EXAMPLES
# Approve the pull request of the current branch
$ gh pr review --approve
# Leave a review comment for the current branch
$ gh pr review --comment -b "interesting"
# Add a review for a specific pull request
$ gh pr review 123
# Request changes on a specific pull request
$ gh pr review 123 -r -b "needs more ASCII art"
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr checkout
Check out a pull request in git
USAGE
gh pr checkout [<number> | <url> | <branch>] [flags]
FLAGS
-b, --branch string Local branch name to use (default [the name of the head branch])
--detach Checkout PR with a detached HEAD
-f, --force Reset the existing local branch to the latest state of the pull request
--recurse-submodules Update all submodules after checkout
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
EXAMPLES
# Interactively select a PR from the 10 most recent to check out
$ gh pr checkout
# Checkout a specific PR
$ gh pr checkout 32
$ gh pr checkout https://github.com/OWNER/REPO/pull/32
$ gh pr checkout feature
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr close
Close a pull request
USAGE
gh pr close {<number> | <url> | <branch>} [flags]
FLAGS
-c, --comment string Leave a closing comment
-d, --delete-branch Delete the local and remote branch after close
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`
## gh pr comment
Add a comment to a GitHub pull request.
Without the body text supplied through flags, the command will interactively
prompt for the comment text.
USAGE
gh pr comment [<number> | <url> | <branch>] [flags]
FLAGS
-b, --body text The comment body text
-F, --body-file file Read body text from file (use "-" to read from standard input)
--create-if-none Create a new comment if no comments are found. Can be used only with --edit-last
--delete-last Delete the last comment of the current user
--edit-last Edit the last comment of the current user
-e, --editor Skip prompts and open the text editor to write the body in
-w, --web Open the web browser to write the comment
--yes Skip the delete confirmation prompt when --delete-last is provided
INHERITED FLAGS
--help Show help for command
-R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format
EXAMPLES
$ gh pr comment 13 --body "Hi from GitHub CLI"
LEARN MORE
Use `gh <command> <subcommand> --help` for more information about a command.
Read the manual at https://cli.github.com/manual
Learn about exit codes using `gh help exit-codes`
Learn about accessibility experiences using `gh help accessibility`

10
.vscode/settings.json vendored
View File

@@ -2,13 +2,5 @@
"prettier.documentSelectors": ["**/*.astro"],
"[astro]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"tailwindCSS.experimental.classRegex": [
["\\b\\w+[cC]lassName\\s*=\\s*[\"']([^\"']*)[\"']"],
["\\b\\w+[cC]lassName\\s*=\\s*`([^`]*)`"],
["[\\w]+[cC]lassName[\"']?\\s*:\\s*[\"']([^\"']*)[\"']"],
["[\\w]+[cC]lassName[\"']?\\s*:\\s*`([^`]*)`"],
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
}

View File

@@ -1,89 +0,0 @@
#!/usr/bin/env python3
import json
import os
import sys
def migrate_content():
"""
Migrate content from content folder to content-migrated folder using mapping file
"""
# Read mapping file
mapping_file = 'migration-mapping.json'
content_dir = 'content'
migrated_dir = 'content-migrated'
try:
with open(mapping_file, 'r') as f:
mapping = json.load(f)
except FileNotFoundError:
print(f"Error: {mapping_file} not found")
return False
except json.JSONDecodeError:
print(f"Error: Invalid JSON in {mapping_file}")
return False
migrated_count = 0
skipped_count = 0
error_count = 0
print(f"Starting migration of {len(mapping)} files...")
for source_path, target_id in mapping.items():
# Determine source file path
if ':' in source_path:
# Nested path like "clean-code-principles:be-consistent"
parts = source_path.split(':')
source_file = os.path.join(content_dir, *parts[:-1], f"{parts[-1]}.md")
else:
# Top level path like "clean-code-principles"
source_file = os.path.join(content_dir, source_path, 'index.md')
# Determine target file path
target_filename = f"{source_path.split(':')[-1]}@{target_id}.md"
target_file = os.path.join(migrated_dir, target_filename)
# Check if target file is empty (needs migration)
if os.path.exists(target_file) and os.path.getsize(target_file) > 0:
print(f"⏭️ Skipped: {target_filename} (already migrated)")
skipped_count += 1
continue
# Check if source file exists
if not os.path.exists(source_file):
print(f"❌ Error: Source file not found: {source_file}")
error_count += 1
continue
try:
# Read source content
with open(source_file, 'r', encoding='utf-8') as f:
content = f.read()
if not content.strip():
print(f"⚠️ Warning: Source file is empty: {source_file}")
continue
# Write to target file
with open(target_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ Migrated: {source_path} -> {target_filename}")
migrated_count += 1
except Exception as e:
print(f"❌ Error migrating {source_path}: {str(e)}")
error_count += 1
print(f"\n📊 Migration Summary:")
print(f" ✅ Migrated: {migrated_count}")
print(f" ⏭️ Skipped: {skipped_count}")
print(f" ❌ Errors: {error_count}")
print(f" 📁 Total: {len(mapping)}")
return error_count == 0
if __name__ == "__main__":
success = migrate_content()
sys.exit(0 if success else 1)

View File

@@ -38,8 +38,6 @@
"@microsoft/clarity": "^1.0.0",
"@nanostores/react": "^1.0.0",
"@napi-rs/image": "^1.9.2",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-popover": "^1.1.14",
"@resvg/resvg-js": "^2.6.2",
"@roadmapsh/editor": "workspace:*",
"@tailwindcss/vite": "^4.1.7",
@@ -73,12 +71,10 @@
"npm-check-updates": "^18.0.1",
"playwright": "^1.52.0",
"prismjs": "^1.30.0",
"radix-ui": "^1.4.2",
"react": "^19.1.0",
"react-calendar-heatmap": "^1.10.0",
"react-confetti": "^6.4.0",
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.8",
"react-resizable-panels": "^3.0.2",
"react-textarea-autosize": "^8.5.9",
"react-tooltip": "^5.28.1",
@@ -92,8 +88,8 @@
"shiki": "^3.4.2",
"slugify": "^1.6.6",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"tippy.js": "^6.3.7",
"tailwindcss": "^4.1.7",
"tiptap-markdown": "^0.8.10",
"turndown": "^7.2.0",
"unified": "^11.0.5",
@@ -121,7 +117,6 @@
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwind-scrollbar": "^4.0.2",
"tsx": "^4.19.4"
}
}

1738
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,22 @@
"aStaDENn5PhEa-cFvNzXa": {
"title": "Mathematics",
"description": "Mathematics is the foundation of AI and Data Science. It is essential to have a good understanding of mathematics to excel in these fields.",
"links": []
"links": [
{
"title": "Mathematics for Machine Learning",
"url": "https://imp.i384100.net/baqMYv",
"type": "article"
},
{
"title": "Algebra and Differential Calculus",
"url": "https://imp.i384100.net/LX5M7M",
"type": "article"
}
]
},
"4WZL_fzJ3cZdWLLDoWN8D": {
"title": "Statistics",
"description": "Statistics is the science of collecting, analyzing, interpreting, presenting, and organizing data. It is a branch of mathematics that deals with the collection, analysis, interpretation, presentation, and organization of data. It is used in a wide range of fields, including science, engineering, medicine, and social science. Statistics is used to make informed decisions, to predict future events, and to test hypotheses. It is also used to summarize data, to describe relationships between variables, and to make inferences about populations based on samples.",
"description": "Statistics is the science of collecting, analyzing, interpreting, presenting, and organizing data. It is a branch of mathematics that deals with the collection, analysis, interpretation, presentation, and organization of data. It is used in a wide range of fields, including science, engineering, medicine, and social science. Statistics is used to make informed decisions, to predict future events, and to test hypotheses. It is also used to summarize data, to describe relationships between variables, and to make inferences about populations based on samples.\n\nLearn more from the resources given on the roadmap.",
"links": []
},
"gWMvD83hVXeTmCuHGIiOL": {
@@ -320,8 +331,24 @@
},
"kBdt_t2SvVsY3blfubWIz": {
"title": "Machine Learning",
"description": "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to \"learn\" (e.g., progressively improve performance on a specific task) from data, without being explicitly programmed. The name machine learning was coined in 1959 by Arthur Samuel. Evolved from the study of pattern recognition and computational learning theory in artificial intelligence, machine learning explores the study and construction of algorithms that can learn from and make predictions on data such algorithms overcome following strictly static program instructions by making data-driven predictions or decisions, through building a model from sample inputs. Machine learning is employed in a range of computing tasks where designing and programming explicit algorithms with good performance is difficult or infeasible; example applications include email filtering, detection of network intruders, and computer vision.",
"links": []
"description": "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to \"learn\" (e.g., progressively improve performance on a specific task) from data, without being explicitly programmed. The name machine learning was coined in 1959 by Arthur Samuel. Evolved from the study of pattern recognition and computational learning theory in artificial intelligence, machine learning explores the study and construction of algorithms that can learn from and make predictions on data such algorithms overcome following strictly static program instructions by making data-driven predictions or decisions, through building a model from sample inputs. Machine learning is employed in a range of computing tasks where designing and programming explicit algorithms with good performance is difficult or infeasible; example applications include email filtering, detection of network intruders, and computer vision.\n\nLearn more from the following resources:",
"links": [
{
"title": "Advantages and Disadvantages of AI",
"url": "https://medium.com/@laners.org/advantages-and-disadvantages-of-artificial-intelligence-cd6e42819b20",
"type": "article"
},
{
"title": "Reinforcement Learning 101",
"url": "https://medium.com/towards-data-science/reinforcement-learning-101-e24b50e1d292",
"type": "article"
},
{
"title": "Understanding AUC-ROC Curve",
"url": "https://medium.com/towards-data-science/understanding-auc-roc-curve-68b2303cc9c5",
"type": "article"
}
]
},
"FdBih8tlGPPy97YWq463y": {
"title": "Classic ML (Sup., Unsup.), Advanced ML (Ensembles, NNs)",

View File

@@ -12,11 +12,6 @@
"title": "AI vs Machine Learning",
"url": "https://www.youtube.com/watch?v=4RixMPF4xis",
"type": "video"
},
{
"title": "AI vs Machine Learning vs Deep Learning vs GenAI",
"url": "https://youtu.be/qYNweeDHiyU?si=eRJXjtk8Q-RKQ8Ms",
"type": "video"
}
]
},

View File

@@ -3,6 +3,11 @@
"title": "AI Security Fundamentals",
"description": "This covers the foundational concepts essential for AI Red Teaming, bridging traditional cybersecurity with AI-specific threats. An AI Red Teamer must understand common vulnerabilities in ML models (like evasion or poisoning), security risks in the AI lifecycle (from data collection to deployment), and how AI capabilities can be misused. This knowledge forms the basis for designing effective tests against AI systems.\n\nLearn more from the following resources:",
"links": [
{
"title": "AI Security | Coursera",
"url": "https://www.coursera.org/learn/ai-security",
"type": "course"
},
{
"title": "Building Trustworthy AI: Contending with Data Poisoning",
"url": "https://nisos.com/research/building-trustworthy-ai/",

View File

@@ -496,10 +496,16 @@
}
]
},
"recycleview@xIvplWfe-uDr9iHjPT1Mx.md": {
"xIvplWfe-uDr9iHjPT1Mx": {
"title": "RecycleView",
"description": "",
"links": []
"description": "RecyclerView is the most commonly used and powerful list management tool in Android development. Witch makes it easy to efficiently display large sets of data. You supply the data and define how each item looks, and the RecyclerView library dynamically creates the elements when they're needed.\n\nAs the name implies, RecyclerView recycles those individual elements. When an item scrolls off the screen, RecyclerView doesn't destroy its view. Instead, RecyclerView reuses the view for new items that have scrolled onscreen. RecyclerView improves performance and your app's responsiveness, and it reduces power consumption.\n\nLearn more from the following resources:",
"links": [
{
"title": "Create Dynamic Lists with RecyclerView",
"url": "https://developer.android.com/develop/ui/views/layout/recyclerview",
"type": "article"
}
]
},
"znvZp24L-PcQwkSObtixs": {
"title": "TextView",

File diff suppressed because it is too large Load Diff

View File

@@ -2247,14 +2247,8 @@
},
"K49M_7gSpfJuZaE6WaHxQ": {
"title": "AutoFixture",
"description": "AutoFixture is an open-source .NET library designed to minimize the 'Arrange' phase of your unit tests by creating object instances automatically with dummy data. It helps reduce boilerplate code and makes tests easier to maintain.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Quick start to AutoFixture",
"url": "https://autofixture.github.io/docs/quick-start/",
"type": "article"
}
]
"description": "",
"links": []
},
"QERTjawqCCCkHfR44Ca0s": {
"title": "Bogus",

File diff suppressed because it is too large Load Diff

View File

@@ -2727,11 +2727,6 @@
"title": "DNS and how it works?",
"description": "DNS (Domain Name System) is a hierarchical, decentralized naming system for computers, services, or other resources connected to the Internet or a private network. It translates human-readable domain names (like `www.example.com`) into IP addresses (like 192.0.2.1) that computers use to identify each other. DNS servers distributed worldwide work together to resolve these queries, forming a global directory service. The system uses a tree-like structure with root servers at the top, followed by top-level domain servers (.com, .org, etc.), authoritative name servers for specific domains, and local DNS servers. DNS is crucial for the functioning of the Internet, enabling users to access websites and services using memorable names instead of numerical IP addresses. It also supports email routing, service discovery, and other network protocols.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Everything You Need to Know About DNS",
"url": "https://cs.fyi/guide/everything-you-need-to-know-about-dns",
"type": "article"
},
{
"title": "What is DNS?",
"url": "https://www.cloudflare.com/en-gb/learning/dns/what-is-dns/",

View File

@@ -3,11 +3,6 @@
"title": "Basic Blockchain Knowledge",
"description": "A blockchain is a decentralized, distributed, and oftentimes public, digital ledger consisting of records called blocks that is used to record transactions across many computers so that any involved block cannot be altered retroactively, without the alteration of all subsequent blocks.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfirn Updraft | Blockchain Basics",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics",
"type": "course"
},
{
"title": "Introduction to Blockchain",
"url": "https://chain.link/education-hub/blockchain",
@@ -68,16 +63,6 @@
"title": "Explore top posts about Blockchain",
"url": "https://app.daily.dev/tags/blockchain?ref=roadmapsh",
"type": "article"
},
{
"title": "Blockchain Architecture Layers: Guide And Topology",
"url": "https://www.cyfrin.io/blog/blockchain-architecture-layers-what-is-it",
"type": "article"
},
{
"title": "Cyfirn Updraft | How Do Blockchains Work?",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics/basics/how-do-blockchains-work?lesson_format=video",
"type": "video"
}
]
},
@@ -147,11 +132,6 @@
"title": "What is Blockchain",
"description": "A blockchain is a decentralized, distributed, and oftentimes public, digital ledger consisting of records called blocks that is used to record transactions across many computers so that any involved block cannot be altered retroactively, without the alteration of all subsequent blocks.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfirn Updraft | Blockchain Basics",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics",
"type": "course"
},
{
"title": "Blockchain Explained",
"url": "https://www.investopedia.com/terms/b/blockchain.asp",
@@ -210,7 +190,7 @@
}
]
},
"why-it-matters@Nc9AH6L7EqeQxh0m6Hddz.md": {
"Nc9AH6L7EqeQxh0m6Hddz": {
"title": "Why it matters?",
"description": "",
"links": []
@@ -219,11 +199,6 @@
"title": "General Blockchain Knowledge",
"description": "A blockchain is a decentralized, distributed ledger technology that records transactions across many computers in such a way that the registered transactions cannot be altered retroactively. This technology is the backbone of cryptocurrencies like Bitcoin and Ethereum, but its applications extend far beyond digital currencies.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfirn Updraft | Blockchain Basics",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics",
"type": "course"
},
{
"title": "The Complete Course On Understanding Blockchain Technology",
"url": "https://www.udemy.com/course/understanding-blockchain-technology/",
@@ -328,11 +303,6 @@
"url": "https://www.nerdwallet.com/article/investing/cryptocurrency",
"type": "article"
},
{
"title": "Cyfrin Glossary | Cryptocurrency",
"url": "https://www.cyfrin.io/glossary/cryptocurrency",
"type": "article"
},
{
"title": "Explore top posts about Crypto",
"url": "https://app.daily.dev/tags/crypto?ref=roadmapsh",
@@ -360,9 +330,9 @@
"type": "article"
},
{
"title": "Cyfrin Updraft | Settin Up a Wallet",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics/basics/setting-up-your-wallet",
"type": "video"
"title": "Choose your wallet - Ethereum",
"url": "https://ethereum.org/en/wallets/find-wallet/",
"type": "article"
}
]
},
@@ -415,11 +385,6 @@
"title": "What Is a Consensus Mechanism?",
"url": "https://www.coindesk.com/learn/what-is-a-consensus-mechanism/",
"type": "article"
},
{
"title": "Consensus Algorithm",
"url": "https://www.cyfrin.io/glossary/consensus-algorithm",
"type": "article"
}
]
},
@@ -868,11 +833,6 @@
"title": "Explore top posts about Blockchain",
"url": "https://app.daily.dev/tags/blockchain?ref=roadmapsh",
"type": "article"
},
{
"title": "Cyfrin Updraft | L1s L2s and Rollups",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics/basics/l1s-l2s-and-rollups",
"type": "video"
}
]
},
@@ -984,11 +944,6 @@
"title": "What Is Chainlink in 5 Minutes",
"url": "https://www.gemini.com/cryptopedia/what-is-chainlink-and-how-does-it-work",
"type": "article"
},
{
"title": "Cyfrin Updraft | Getting real world price data from chainlink",
"url": "https://updraft.cyfrin.io/courses/solidity/fund-me/getting-prices-from-chainlink",
"type": "video"
}
]
},
@@ -1015,11 +970,6 @@
"title": "Explore top posts about Oracle",
"url": "https://app.daily.dev/tags/oracle?ref=roadmapsh",
"type": "article"
},
{
"title": "Cyfrin Updraft | Intro to oracles",
"url": "https://updraft.cyfrin.io/courses/solidity/fund-me/real-world-price-data",
"type": "video"
}
]
},
@@ -1027,11 +977,6 @@
"title": "Smart Contracts",
"description": "A smart contract is a computer program or a transaction protocol that is intended to automatically execute, control or document legally relevant events and actions according to the terms of a contract or an agreement.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfirn Updraft | Solidity Smart Contract Development",
"url": "https://updraft.cyfrin.io/courses/solidity",
"type": "course"
},
{
"title": "Smart Contracts",
"url": "https://www.ibm.com/topics/smart-contracts",
@@ -1063,11 +1008,6 @@
"title": "Solidity",
"description": "Solidity is an object-oriented programming language created specifically by Ethereum Network team for constructing smart contracts on various blockchain platforms, most notably, Ethereum. It's used to create smart contracts that implements business logic and generate a chain of transaction records in the blockchain system. It acts as a tool for creating machine-level code and compiling it on the Ethereum Virtual Machine (EVM).\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfirn Updraft | Solidity Smart Contract Development",
"url": "https://updraft.cyfrin.io/courses/solidity",
"type": "course"
},
{
"title": "Solidity Programming Language",
"url": "https://soliditylang.org/",
@@ -1104,11 +1044,6 @@
"title": "Vyper",
"description": "Vyper is a contract-oriented, pythonic programming language that targets the Ethereum Virtual Machine (EVM).\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Vyper Smart Contract Development",
"url": "https://updraft.cyfrin.io/courses/intro-python-vyper-smart-contract-development",
"type": "course"
},
{
"title": "Vyper Programming Language",
"url": "https://vyper.readthedocs.io/en/stable/",
@@ -1228,11 +1163,6 @@
"title": "Explore top posts about CI/CD",
"url": "https://app.daily.dev/tags/cicd?ref=roadmapsh",
"type": "article"
},
{
"title": "Cyfrin Updraft | Deploying Your First Smart Contract",
"url": "https://updraft.cyfrin.io/courses/solidity/simple-storage/deploying-solidity-smart-contract",
"type": "video"
}
]
},
@@ -1275,11 +1205,6 @@
"title": "Upgrading your Smart Contracts | A Tutorial & Introduction",
"url": "https://youtu.be/bdXJmWajZRY",
"type": "video"
},
{
"title": "Cyfrin Updraft | Introduction to Upgradable Smart Contracts",
"url": "https://updraft.cyfrin.io/courses/advanced-foundry/upgradeable-smart-contracts/introduction-to-upgradeable-smart-contracts",
"type": "video"
}
]
},
@@ -1306,21 +1231,6 @@
"title": "ERC-1155 Token Standard (Multi-Token)",
"url": "https://decrypt.co/resources/what-is-erc-1155-ethereums-flexible-token-standard",
"type": "article"
},
{
"title": "ERC3675",
"url": "https://www.cyfrin.io/glossary/erc-3675",
"type": "article"
},
{
"title": "ERC4337",
"url": "https://www.cyfrin.io/glossary/erc-4337",
"type": "article"
},
{
"title": "Cyfrin Updraft | Introduction To ERC Fundamentals and ERC20",
"url": "https://updraft.cyfrin.io/courses/advanced-foundry/How-to-create-an-erc20-crypto-currency/erc-and-erc20-fundamentals",
"type": "video"
}
]
},
@@ -1328,11 +1238,6 @@
"title": "Crypto Wallets",
"description": "A cryptocurrency wallet is a device, physical medium, program, or service which stores the public and/or private keys for cryptocurrency transactions. In addition to this basic function of storing the keys, a cryptocurrency wallet more often also offers the functionality of encrypting and/or signing information.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfrin Updraft | Smart Contract DevOps",
"url": "https://updraft.cyfrin.io/courses/wallets",
"type": "course"
},
{
"title": "What is a Crypto Wallet?: A Beginners Guide",
"url": "https://crypto.com/university/crypto-wallets",
@@ -1347,6 +1252,11 @@
"title": "Choose your wallet - Ethereum",
"url": "https://ethereum.org/en/wallets/find-wallet/",
"type": "article"
},
{
"title": "Explore top posts about Crypto",
"url": "https://app.daily.dev/tags/crypto?ref=roadmapsh",
"type": "article"
}
]
},
@@ -1410,11 +1320,6 @@
"title": "Explore top posts about Decentralized",
"url": "https://app.daily.dev/tags/decentralized?ref=roadmapsh",
"type": "article"
},
{
"title": "Cyfrin Updraft | Introduction to IPFS",
"url": "https://updraft.cyfrin.io/courses/advanced-foundry/how-to-create-an-NFT-collection/what-is-ipfs",
"type": "video"
}
]
},
@@ -1506,16 +1411,6 @@
"title": "Foundry",
"description": "Foundry is a smart contract development toolchain. Foundry manages your dependencies, compiles your project, runs tests, deploys, and lets you interact with the chain from the command-line and via Solidity scripts.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfrin Updraft | Foundry Fundamentals",
"url": "https://updraft.cyfrin.io/courses/foundry",
"type": "course"
},
{
"title": "Cyfrin Updraft | Foundry Advanced",
"url": "https://updraft.cyfrin.io/courses/advanced-foundry",
"type": "course"
},
{
"title": "Foundry Overview",
"url": "https://book.getfoundry.sh/",
@@ -1532,11 +1427,6 @@
"title": "Security",
"description": "Smart contracts are extremely flexible, capable of both holding large quantities of tokens (often in excess of $1B) and running immutable logic based on previously deployed smart contract code. While this has created a vibrant and creative ecosystem of trustless, interconnected smart contracts, it is also the perfect ecosystem to attract attackers looking to profit by exploiting vulnerabilities\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfrin Updraft | Smart Contract Security",
"url": "https://updraft.cyfrin.io/courses/security",
"type": "course"
},
{
"title": "Smart Contract Security",
"url": "https://ethereum.org/en/developers/docs/smart-contracts/security/",
@@ -1558,11 +1448,6 @@
"title": "Practices",
"description": "Smart contract programming requires a different engineering mindset. The cost of failure can be high, and change can be difficult.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cyfrin Updraft | Smart Contract Security",
"url": "https://updraft.cyfrin.io/courses/security",
"type": "course"
},
{
"title": "Ethereum Smart Contract Security Best Practices",
"url": "https://consensys.github.io/smart-contract-best-practices/",
@@ -1598,11 +1483,6 @@
"title": "Smart Contract Fuzzing",
"url": "https://youtu.be/LRyyNzrqgOc",
"type": "video"
},
{
"title": "Cyfrin Updraft | Stateful And Stateless Fuzzing",
"url": "https://updraft.cyfrin.io/courses/security/tswap/stateful-and-stateless-fuzzing",
"type": "video"
}
]
},
@@ -1656,11 +1536,6 @@
"title": "Top 10 Tools for Blockchain Development",
"url": "https://www.blockchain-council.org/blockchain/top-10-tools-for-blockchain-development/",
"type": "article"
},
{
"title": "Cyfrin Updraft | Security Tools",
"url": "https://updraft.cyfrin.io/courses/security/audit/tools",
"type": "video"
}
]
},
@@ -1677,11 +1552,6 @@
"title": "Slither Framework",
"url": "https://blog.trailofbits.com/2018/10/19/slither-a-solidity-static-analysis-framework/",
"type": "article"
},
{
"title": "Cyfrin Updraft | Slither Walkthrough",
"url": "https://updraft.cyfrin.io/courses/security/puppy-raffle/slither-walkthrough",
"type": "video"
}
]
},
@@ -1945,11 +1815,6 @@
"url": "https://www.coindesk.com/learn/what-is-a-dapp-decentralized-apps-explained/",
"type": "article"
},
{
"title": "Cyfrin Glossary | dApp",
"url": "https://www.cyfrin.io/glossary/dapp",
"type": "article"
},
{
"title": "Explore Top dApps on Ethereum and its Layer 2s",
"url": "https://www.ethereum-ecosystem.com/apps",
@@ -2053,11 +1918,6 @@
"title": "NFT Explained In 5 Minutes | What Is NFT? - Non Fungible Token",
"url": "https://youtu.be/NNQLJcJEzv0",
"type": "video"
},
{
"title": "Cyfrin Updraft | What is an NFT",
"url": "https://updraft.cyfrin.io/courses/advanced-foundry/how-to-create-an-NFT-collection/what-is-a-nft",
"type": "video"
}
]
},
@@ -2117,11 +1977,6 @@
"title": "Alchemy",
"url": "https://www.alchemy.com/",
"type": "article"
},
{
"title": "Cyfrin Updraft | Introduction to Alchemy",
"url": "https://updraft.cyfrin.io/courses/foundry/foundry-simple-storage/introduction-to-alchemy",
"type": "video"
}
]
},
@@ -2756,11 +2611,6 @@
"title": "Introduction to zk-SNARKs",
"url": "https://vitalik.eth.limo/general/2021/01/26/snarks.html",
"type": "article"
},
{
"title": "What Are zk-SNARKs and zkSTARKs: Full Comparison",
"url": "https://www.cyfrin.io/blog/a-full-comparison-what-are-zk-snarks-and-zk-starks",
"type": "article"
}
]
},
@@ -2862,11 +2712,6 @@
"title": "Explore top posts about Blockchain",
"url": "https://app.daily.dev/tags/blockchain?ref=roadmapsh",
"type": "article"
},
{
"title": "Cyfirn Updraft | The Purpose of Smart Contracts",
"url": "https://updraft.cyfrin.io/courses/blockchain-basics/basics/the-purpose-of-smart-contracts",
"type": "video"
}
]
},
@@ -2889,11 +2734,6 @@
"url": "https://docs.ipfs.tech/concepts/how-ipfs-works/",
"type": "article"
},
{
"title": "Data Locations - Storage, Memory, and Calldata",
"url": "https://www.cyfrin.io/glossary/data-locations-storage-memory-and-calldata-solidity-code-example",
"type": "article"
},
{
"title": "Explore top posts about Storage",
"url": "https://app.daily.dev/tags/storage?ref=roadmapsh",

View File

@@ -178,35 +178,13 @@
},
"sgfqb22sdN4VRJYkhAVaf": {
"title": "Function Overloading",
"description": "Function overloading in C++ allows multiple functions to share the same name, provided they differ in the number or types of parameters. This facilitates compile-time polymorphism, enhancing code readability and maintainability by enabling functions to perform similar operations on different data types or argument counts.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Function Overloading - Microsoft Learn",
"url": "https://learn.microsoft.com/en-us/cpp/cpp/function-overloading",
"type": "article"
},
{
"title": "C++ Function Overloading - W3Schools",
"url": "https://www.w3schools.com/cpp/cpp_function_overloading.asp",
"type": "article"
}
]
"description": "",
"links": []
},
"llCBeut_uc9IAe2oi4KZ9": {
"title": "Operator Overloading",
"description": "Operator overloading in C++ is a feature that allows you to redefine the way operators work for user-defined types (such as classes and structs). It lets you specify how operators like +, -, \\*, ==, etc., behave when applied to objects of your class. Visit the following resources to learn more:\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Operator Overloading - Microsoft Learn",
"url": "https://learn.microsoft.com/en-us/cpp/cpp/operator-overloading",
"type": "article"
},
{
"title": "operator overloading - cppreference.com",
"url": "https://en.cppreference.com/w/cpp/language/operators",
"type": "article"
}
]
"description": "Operators in C++ are symbols that perform various operations on data, such as arithmetic, comparison, and logical operations. They are used to manipulate and evaluate expressions and variables.\n\nHere is a list of the commonly used operator types in C++:\n\n* **Arithmetic Operators**: These are used for performing arithmetic operations like addition, subtraction, multiplication, and division.\n \n * `+`: addition\n \n int sum = 5 + 3; // sum will be 8\n \n \n * `-`: subtraction\n \n int difference = 5 - 3; // difference will be 2\n \n \n * `*`: multiplication\n \n int product = 5 * 3; // product will be 15\n \n \n * `/`: division\n \n int quotient = 15 / 3; // quotient will be 5\n \n \n * `%`: modulo (remainder)\n \n int remainder = 7 % 3; // remainder will be 1\n \n \n* **Comparison (Relational) Operators**: These are used to compare two values and return true or false based on the comparison.\n \n * `==`: equal to\n \n bool isEqual = (5 == 3); // isEqual will be false\n \n \n * `!=`: not equal to\n \n bool isNotEqual = (5 != 3); // isNotEqual will be true\n \n \n * `<`: less than\n \n bool isLess = (5 < 3); // isLess will be false\n \n \n * `>`: greater than\n \n bool isGreater = (5 > 3); // isGreater will be true\n \n \n * `<=`: less than or equal to\n \n bool isLessOrEqual = (5 <= 3); // isLessOrEqual will be false\n \n \n * `>=`: greater than or equal to\n \n bool isGreaterOrEqual = (5 >= 3); // isGreaterOrEqual will be true\n \n \n* **Logical Operators**: These operators are used to perform logical operations such as AND (&&), OR (||), and NOT (!) on boolean values.\n \n * `&&`: logical AND\n \n bool result = (true && false); // result will be false\n \n \n * `||`: logical OR\n \n bool result = (true || false); // result will be true\n \n \n * `!`: logical NOT\n \n bool result = !false; // result will be true\n \n \n* **Assignment Operators**: These are used to assign values to variables.\n \n * `=`: simple assignment\n \n int x = 5; // x gets the value 5\n \n \n * `+=`: addition assignment\n \n int x = 5;\n x += 3; // x gets the value 8 (5 + 3)\n \n \n * `-=`: subtraction assignment\n \n int x = 5;\n x -= 3; // x gets the value 2 (5 - 3)\n \n \n * `*=`: multiplication assignment\n \n int x = 5;\n x *= 3; // x gets the value 15 (5 * 3)\n \n \n * `/=`: division assignment\n \n int x = 15;\n x /= 3; // x gets the value 5 (15 / 3)\n \n \n * `%=`: modulo assignment\n \n int x = 7;\n x %= 3; // x gets the value 1 (7 % 3)\n \n \n\nThese are some of the main operator categories in C++. Each operator allows you to perform specific operations, making your code more efficient and concise.",
"links": []
},
"xjiFBVe-VGqCqWfkPVGKf": {
"title": "Lambdas",
@@ -491,19 +469,8 @@
},
"DHdNBP7_ixjr6h-dIQ7g6": {
"title": "Standard Library + STL",
"description": "The C++ Standard Template Library (STL) is a collection of header files that provide several data structures, algorithms, and functions to simplify your C++ coding experience. The primary purpose of the STL is to save time and increase efficiency by providing a ready-to-use set of useful tools. The most commonly used features of the STL can be divided into three main categories: containers, algorithms, and iterators.\n\nContainers\n----------\n\nContainers are the data structures used for data storage and manipulation in C++. They are classified into four types: sequence containers, associative containers, unordered associative containers, and container adaptors.\n\n* **Sequence Containers**: These are linear data structures that store elements in a sequential manner. Examples include:\n \n * `std::vector`: A dynamic array that grows and shrinks at runtime.\n \n std::vector<int> my_vector;\n \n \n * `std::list`: A doubly linked list.\n \n std::list<int> my_list;\n \n \n * `std::deque`: A double-ended queue allowing insertion and deletion at both ends.\n \n std::deque<int> my_deque;\n \n \n* **Associative Containers**: These containers store data in a sorted manner with unique keys. Examples include:\n \n * `std::set`: A collection of unique elements sorted by keys.\n \n std::set<int> my_set;\n \n \n * `std::map`: A collection of key-value pairs sorted by keys.\n \n std::map<std::string, int> my_map;\n \n \n* **Unordered Associative Containers**: These containers store data in an unordered manner using hash tables. Examples include:\n \n * `std::unordered_set`: A collection of unique elements in no specific order.\n \n std::unordered_set<int> my_unordered_set;\n \n \n * `std::unordered_map`: A collection of key-value pairs in no specific order.\n \n std::unordered_map<std::string, int> my_unordered_map;\n \n \n* **Container Adaptors**: These are containers based on other existing containers. Examples include:\n \n * `std::stack`: A LIFO data structure based on deque or list.\n \n std::stack<int> my_stack;\n \n \n * `std::queue`: A FIFO data structure based on deque or list.\n \n std::queue<int> my_queue;\n \n \n * `std::priority_queue`: A sorted queue based on vector or deque.\n \n std::priority_queue<int> my_priority_queue;\n \n \n\nAlgorithms\n----------\n\nThe STL provides several generic algorithms that can be used to perform various operations on the data stored in containers. They are divided into five categories: non-modifying sequence algorithms, modifying sequence algorithms, sorting algorithms, sorted range algorithms, and numeric algorithms.\n\nSome examples include `std::find`, `std::replace`, `std::sort`, and `std::binary_search`.\n\nFor example, to sort a vector, you can use the following code:\n\n std::vector<int> my_vec = {4, 2, 5, 1, 3};\n std::sort(my_vec.begin(), my_vec.end());\n \n\nIterators\n---------\n\nIterators are a fundamental concept in the STL, as they provide a unified way to access elements in containers. Iterators can be thought of as an advanced form of pointers.\n\nEach container has its own iterator type, which can be used to traverse elements and modify values. The most common iterator operations are `begin()` and `end()` for getting iterators pointing to the first and one past the last element of a container, respectively.\n\nFor example, to iterate through a vector and print its elements, you can use the following code:\n\n std::vector<int> my_vec = {1, 2, 3, 4, 5};\n for (auto it = my_vec.begin(); it != my_vec.end(); ++it) {\n std::cout << *it << \" \";\n }\n \n\nThis is just a brief overview of the C++ Standard Template Library. There are many other features and functions available in the STL, and familiarizing yourself with them is crucial for efficient C++ programming.\n\nLearn more from the following resources:",
"links": [
{
"title": "Mastering STL in C++23: New Features, Updates, and Best Practices",
"url": "https://simplifycpp.org/books/Mastering_STL.pdf",
"type": "article"
},
{
"title": "C++ Standard Template Library (STL) Short Overview",
"url": "https://www.youtube.com/watch?v=Id6ZEb_Lg58",
"type": "video"
}
]
"description": "The C++ Standard Template Library (STL) is a collection of header files that provide several data structures, algorithms, and functions to simplify your C++ coding experience. The primary purpose of the STL is to save time and increase efficiency by providing a ready-to-use set of useful tools. The most commonly used features of the STL can be divided into three main categories: containers, algorithms, and iterators.\n\nContainers\n----------\n\nContainers are the data structures used for data storage and manipulation in C++. They are classified into four types: sequence containers, associative containers, unordered associative containers, and container adaptors.\n\n* **Sequence Containers**: These are linear data structures that store elements in a sequential manner. Examples include:\n \n * `std::vector`: A dynamic array that grows and shrinks at runtime.\n \n std::vector<int> my_vector;\n \n \n * `std::list`: A doubly linked list.\n \n std::list<int> my_list;\n \n \n * `std::deque`: A double-ended queue allowing insertion and deletion at both ends.\n \n std::deque<int> my_deque;\n \n \n* **Associative Containers**: These containers store data in a sorted manner with unique keys. Examples include:\n \n * `std::set`: A collection of unique elements sorted by keys.\n \n std::set<int> my_set;\n \n \n * `std::map`: A collection of key-value pairs sorted by keys.\n \n std::map<std::string, int> my_map;\n \n \n* **Unordered Associative Containers**: These containers store data in an unordered manner using hash tables. Examples include:\n \n * `std::unordered_set`: A collection of unique elements in no specific order.\n \n std::unordered_set<int> my_unordered_set;\n \n \n * `std::unordered_map`: A collection of key-value pairs in no specific order.\n \n std::unordered_map<std::string, int> my_unordered_map;\n \n \n* **Container Adaptors**: These are containers based on other existing containers. Examples include:\n \n * `std::stack`: A LIFO data structure based on deque or list.\n \n std::stack<int> my_stack;\n \n \n * `std::queue`: A FIFO data structure based on deque or list.\n \n std::queue<int> my_queue;\n \n \n * `std::priority_queue`: A sorted queue based on vector or deque.\n \n std::priority_queue<int> my_priority_queue;\n \n \n\nAlgorithms\n----------\n\nThe STL provides several generic algorithms that can be used to perform various operations on the data stored in containers. They are divided into five categories: non-modifying sequence algorithms, modifying sequence algorithms, sorting algorithms, sorted range algorithms, and numeric algorithms.\n\nSome examples include `std::find`, `std::replace`, `std::sort`, and `std::binary_search`.\n\nFor example, to sort a vector, you can use the following code:\n\n std::vector<int> my_vec = {4, 2, 5, 1, 3};\n std::sort(my_vec.begin(), my_vec.end());\n \n\nIterators\n---------\n\nIterators are a fundamental concept in the STL, as they provide a unified way to access elements in containers. Iterators can be thought of as an advanced form of pointers.\n\nEach container has its own iterator type, which can be used to traverse elements and modify values. The most common iterator operations are `begin()` and `end()` for getting iterators pointing to the first and one past the last element of a container, respectively.\n\nFor example, to iterate through a vector and print its elements, you can use the following code:\n\n std::vector<int> my_vec = {1, 2, 3, 4, 5};\n for (auto it = my_vec.begin(); it != my_vec.end(); ++it) {\n std::cout << *it << \" \";\n }\n \n\nThis is just a brief overview of the C++ Standard Template Library. There are many other features and functions available in the STL, and familiarizing yourself with them is crucial for efficient C++ programming.",
"links": []
},
"Ebu8gzbyyXEeJryeE0SpG": {
"title": "Iterators",
@@ -622,7 +589,7 @@
},
"R2-qWGUxsTOeSHRuUzhd2": {
"title": "C++ 17",
"description": "C++17, also known as C++1z, is the version of the C++ programming language published in December 2017. It builds upon the previous standard, C++14, and adds various new features and enhancements to improve the language's expressiveness, performance, and usability.\n\nKey Features:\n-------------\n\n* If-init-statement: Introduces a new syntax for writing conditions with scope inside if and switch statements.\n\n if (auto it = map.find(key); it != map.end())\n {\n // Use it\n }\n \n\n* Structured Binding Declarations: Simplify the process of unpacking a tuple, pair, or other aggregate types.\n\n map<string, int> data;\n auto [iter, success] = data.emplace(\"example\", 42);\n \n\n* Inline variables: Enables `inline` keyword for variables and allows single definition of global and class static variables in header files.\n\n inline int globalVar = 0;\n \n\n* Folds expressions: Introduce fold expressions for variadic templates.\n\n template <typename... Ts>\n auto sum(Ts... ts)\n {\n return (ts + ...);\n }\n \n\n* constexpr if statement: Allows conditional compilation during compile time.\n\n template <typename T>\n auto get_value(T t)\n {\n if constexpr (std::is_pointer_v<T>)\n {\n return *t;\n }\n else\n {\n return t;\n }\n }\n \n\n* Improved lambda expression: Allows lambda to capture a single object without changing its type or constness.\n\n auto func = [x = std::move(obj)] { /* use x */ };\n \n\n* Standard file system library: `std::filesystem` as a standardized way to manipulate paths, directories, and files.\n \n* New Standard Library additions: `<string_view>` (non-owning string reference), `<any>` (type-erased container), `<optional>` (optional value wrapper), `<variant>` (type-safe discriminated union / sum type), and `<memory_resource>` (library for polymorphic allocators).\n \n* Parallel Algorithms: Adds support for parallel execution of Standard Library algorithms.\n \n\nThis is a brief summary of the key features of C++17; it includes more features and library updates. For a complete list, you can refer to the [full list of C++17 features and changes](https://en.cppreference.com/w/cpp/17).",
"description": "C++17, also known as C++1z, is the version of the C++ programming language published in December 2017. It builds upon the previous standard, C++14, and adds various new features and enhancements to improve the language's expressiveness, performance, and usability.\n\nKey Features:\n-------------\n\n* If-init-statement: Introduces a new syntax for writing conditions with scope inside if and switch statements.\n\n if (auto it = map.find(key); it != map.end())\n {\n // Use it\n }\n \n\n* Structured Binding Declarations: Simplify the process of unpacking a tuple, pair, or other aggregate types.\n\n map<string, int> data;\n auto [iter, success] = data.emplace(\"example\", 42);\n \n\n* Inline variables: Enables `inline` keyword for variables and allows single definition of global and class static variables in header files.\n\n inline int globalVar = 0;\n \n\n* Folds expressions: Introduce fold expressions for variadic templates.\n\n template <typename... Ts>\n auto sum(Ts... ts)\n {\n return (ts + ...);\n }\n \n\n* constexpr if statement: Allows conditional compilation during compile time.\n\n template <typename T>\n auto get_value(T t)\n {\n if constexpr (std::is_pointer_v<T>)\n {\n return *t;\n }\n else\n {\n return t;\n }\n }\n \n\n* Improved lambda expression: Allows lambda to capture a single object without changing its type or constness.\n\n auto func = [x = std::move(obj)] { /* use x */ };\n \n\n* Standard file system library: `std::filesystem` as a standardized way to manipulate paths, directories, and files.\n \n* New Standard Library additions: `<string_view>` (non-owning string reference), `<any>` (type-safe discrimination union), `<optional>` (optional value wrapper), `<variant>` (type-safe sum type), and `<memory_resource>` (library for polymorphic allocators).\n \n* Parallel Algorithms: Adds support for parallel execution of Standard Library algorithms.\n \n\nThis is a brief summary of the key features of C++17; it includes more features and library updates. For a complete list, you can refer to the [full list of C++17 features and changes](https://en.cppreference.com/w/cpp/17).",
"links": []
},
"o3no4a5_iMFzEAGs56-BJ": {

View File

@@ -3804,7 +3804,7 @@
},
"zqRaMmqcLfx400kJ-h0LO": {
"title": "Zero Day",
"description": "A zero-day vulnerability is a software security flaw unknown to the vendor and its developers, leaving it unpatched and potentially exploitable. When attackers discover and exploit such a vulnerability before the software creator can develop and release a fix, it's called a zero-day attack. These attacks are particularly dangerous because they take advantage of the window between discovery and patching, during which systems are highly vulnerable. Zero-days are prized in cybercriminal circles and can be used for various malicious purposes, including data theft, system compromise, or as part of larger attack campaigns. Defending against zero-days often requires proactive security measures, as traditional signature-based defenses are ineffective against unknown threats.\n\nLearn more from the following resources:",
"description": "A zero-day vulnerability is a software security flaw unknown to the vendor and exploit developers, leaving it unpatched and potentially exploitable. When attackers discover and exploit such a vulnerability before the software creator can develop and release a fix, it's called a zero-day attack. These attacks are particularly dangerous because they take advantage of the window between discovery and patching, during which systems are highly vulnerable. Zero-days are prized in cybercriminal circles and can be used for various malicious purposes, including data theft, system compromise, or as part of larger attack campaigns. Defending against zero-days often requires proactive security measures, as traditional signature-based defenses are ineffective against unknown threats.\n\nLearn more from the following resources:",
"links": [
{
"title": "What is a Zero-day Attack?",
@@ -4116,7 +4116,7 @@
},
"v9njgIxZyabJZ5iND3JGc": {
"title": "Zero day",
"description": "A zero-day vulnerability is a software security flaw unknown to the vendor and its developers, leaving it unpatched and potentially exploitable. When attackers discover and exploit such a vulnerability before the software creator can develop and release a fix, it's called a zero-day attack. These attacks are particularly dangerous because they take advantage of the window between discovery and patching, during which systems are highly vulnerable. Zero-days are prized in cybercriminal circles and can be used for various malicious purposes, including data theft, system compromise, or as part of larger attack campaigns. Defending against zero-days often requires proactive security measures, as traditional signature-based defenses are ineffective against unknown threats.\n\nLearn more from the following resources:",
"description": "A zero-day vulnerability is a software security flaw unknown to the vendor and exploit developers, leaving it unpatched and potentially exploitable. When attackers discover and exploit such a vulnerability before the software creator can develop and release a fix, it's called a zero-day attack. These attacks are particularly dangerous because they take advantage of the window between discovery and patching, during which systems are highly vulnerable. Zero-days are prized in cybercriminal circles and can be used for various malicious purposes, including data theft, system compromise, or as part of larger attack campaigns. Defending against zero-days often requires proactive security measures, as traditional signature-based defenses are ineffective against unknown threats.\n\nLearn more from the following resources:",
"links": [
{
"title": "What is a Zero-day Attack?",

File diff suppressed because it is too large Load Diff

View File

@@ -616,7 +616,7 @@
"type": "article"
},
{
"title": "Mercurial",
"title": "Mecurial",
"url": "https://www.mercurial-scm.org/",
"type": "article"
},
@@ -2850,8 +2850,8 @@
"description": "GitOps is a paradigm for managing infrastructure and application deployments using Git as the single source of truth. It extends DevOps practices by using Git repositories to store declarative descriptions of infrastructure and applications. Changes to the desired state are made through pull requests, which trigger automated processes to align the actual state with the desired state. GitOps relies on continuous deployment tools that automatically reconcile the live system with the desired state defined in Git. This approach provides benefits such as version control for infrastructure, improved auditability, easier rollbacks, and enhanced collaboration. GitOps is particularly well-suited for cloud-native applications and Kubernetes environments, offering a streamlined method for managing complex, distributed systems.\n\nVisit the following resources to learn more:",
"links": [
{
"title": " GitOps",
"url": "https://www.gitops.tech/",
"title": "Guide to GitOps",
"url": "https://www.weave.works/technologies/gitops/",
"type": "article"
},
{

View File

@@ -1694,7 +1694,7 @@
},
"jzYjHx-gIKSP8dQUTqWVw": {
"title": "commit-msg",
"description": "The commit-msg hook is a client-side hook that runs after you enter a commit message, but before the commit is finalized in your repository. It's typically used to validate or modify the commit message before it's recorded in the Git history.\n\nVisit the following resources to learn more:",
"description": "The commit-msg hook is a client-side hook that runs after you've committed changes to your repository. It's typically used to validate or modify the commit message before it's recorded in the Git history.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "A Git-Hook for Commit Messages Validation - No Husky, Just JS",

View File

@@ -2370,7 +2370,7 @@
},
"RuXuHQhMt2nywk43LgGeJ": {
"title": "Static Library",
"description": "Static libraries in iOS development are collections of compiled code that are linked directly into an app's executable at build time. They contain object code that becomes part of the final application binary, increasing its size but potentially improving load time performance. Static libraries are typically distributed as .a files, often accompanied by header files that define their public interfaces. Using static libraries ensures that all necessary code is available within the app, eliminating runtime dependencies. Static libraries are particularly useful for distributing closed-source code or when aiming to minimize runtime overhead. They offer simplicity in distribution and version management but may require recompilation of the entire app when the library is updated. In iOS development, static libraries are gradually being replaced by more flexible options like dynamic frameworks and XCFrameworks, especially for larger or frequently updated libraries.\n\nLearn more from the following resources:",
"description": "Static libraries in iOS development are collections of compiled code that are linked directly into an app's executable at build time. They contain object code that becomes part of the final application binary, increasing its size but potentially improving load time performance. Static libraries are typically distributed as .a files, often accompanied by header files that define their public interfaces. When using static libraries, the entire library code is included in the app, even if only a portion is used, which can lead to larger app sizes. However, this approach ensures that all necessary code is available within the app, eliminating runtime dependencies. Static libraries are particularly useful for distributing closed-source code or when aiming to minimize runtime overhead. They offer simplicity in distribution and version management but may require recompilation of the entire app when the library is updated. In iOS development, static libraries are gradually being replaced by more flexible options like dynamic frameworks and XCFrameworks, especially for larger or frequently updated libraries.\n\nLearn more from the following resources:",
"links": [
{
"title": "Static Library in iOS",

View File

@@ -1034,11 +1034,6 @@
"title": "Java Cryptography Tutorial",
"url": "https://jenkov.com/tutorials/java-cryptography/index.html",
"type": "article"
},
{
"title": "Cryptography 101 for Java developers",
"url": "https://www.youtube.com/watch?v=itmNhVckTPc",
"type": "video"
}
]
},

View File

@@ -1344,6 +1344,11 @@
"title": "JavaScript MDN Docs",
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#comparison_operators",
"type": "article"
},
{
"title": "Comparison operators",
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#comparison_operators",
"type": "article"
}
]
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,936 +0,0 @@
{
"y7KjVfSI6CAduyHd4mBFT": {
"title": "Navigation Basics",
"description": "In Linux, navigation between directories and files is a fundamental, yet essential function that allows you to exploit the power of the command-line interface (CLI). Mastering the basic Linux navigation commands such as `cd`, `pwd`, `ls`, and `tree` enables you to flawlessly move from one point to another within the filesystem, display the list of files & directories, and understand your position relative to other system components.\n\nHere is how you use these commands:\n\n* To change directories, use the `cd` command:\n\n cd /path/to/directory\n \n\n* To list the contents of a directory, use the `ls` command:\n\n ls\n \n\nVisit the following resources to learn more:",
"links": [
{
"title": "Linux for Noobs (Hands-on)",
"url": "https://labex.io/courses/linux-for-noobs",
"type": "course"
},
{
"title": "Intro to Linux",
"url": "https://www.linkedin.com/pulse/intro-linux-fundamentals-what-hillary-nyakundi-4u7af/",
"type": "article"
},
{
"title": "Practice on Linux fundamentals",
"url": "https://linuxjourney.com/",
"type": "article"
},
{
"title": "Linux fundamentals",
"url": "https://www.youtube.com/watch?v=kPylihJRG70&t=1381s&ab_channel=TryHackMe",
"type": "video"
}
]
},
"qLeEEwBvlGt1fP5Qcreah": {
"title": "Basic Commands",
"description": "Linux Navigation Basics is about using simple commands to move around and manage files on your computer. For example, cd lets you go into different folders, ls shows you what files and folders are inside, and pwd tells you where you are currently. These commands help you easily find and organize your files.\n\n # Change directory\n cd /path/to/directory \n \n # Lists files and directories in the current directory.\n ls \n \n # View current working directory\n pwd \n \n # Displays the mannual page for a command\n man ls\n \n\nIn this brief introduction, we will discuss and explore these basic commands and how they aid us in navigation around the Linux environment.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux pwd Command: Directory Displaying",
"url": "https://labex.io/tutorials/linux-file-and-directory-operations-17997",
"type": "article"
}
]
},
"q-Ky0ietZGpyUcBQfh-BJ": {
"title": "Moving Files / Directories",
"description": "The `mv` command moves files and directories between locations and can also rename them. Use syntax `mv [options] source destination` where source is the file/directory to move and destination is the target location. This versatile command is essential for file organization and management in Linux systems.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux mv Command: File Moving and Renaming",
"url": "https://labex.io/tutorials/linux-linux-mv-command-file-moving-and-renaming-209743",
"type": "article"
}
]
},
"9oo2fxTM2_p0VYPBroqxa": {
"title": "Creating & Deleting Files / Dirs",
"description": "Linux file operations include creating files with `touch` (empty files) or `cat > filename` (with content) and deleting with `rm filename`. Use `rm -i` for confirmation prompts and `rmdir` for empty directories. File deletion is permanent - no recycle bin. Essential commands for basic file management and system administration.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux rm Command: File Removing",
"url": "https://labex.io/tutorials/linux-linux-rm-command-file-removing-209741",
"type": "article"
}
]
},
"3fzuXKH7az_LVnmnoXB1p": {
"title": "Directory Hierarchy Overview",
"description": "In Linux, understanding the directory hierarchy is crucial for efficient navigation and file management. A Linux system's directory structure, also known as the Filesystem Hierarchy Standard (FHS), is a defined tree structure that helps to prevent files from being scattered all over the system and instead organise them in a logical and easy-to-navigate manner.\n\n* `/`: Root directory, the top level of the file system.\n* `/home`: User home directories.\n* `/bin`: Essential binary executables.\n* `/sbin`: System administration binaries.\n* `/etc`: Configuration files.\n* `/var`: Variable data (logs, spool files).\n* `/usr`: User programs and data.\n* `/lib`: Shared libraries.\n* `/tmp`: Temporary files.\n* `/opt`: Third-party applications.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Overview of File System Hierarchy Standard (FHS)",
"url": "https://access.redhat.com/documentation/ru-ru/red_hat_enterprise_linux/4/html/reference_guide/s1-filesystem-fhs#s3-filesystem-usr",
"type": "article"
},
{
"title": "The Linux File System explained in 1,233 seconds",
"url": "https://youtu.be/A3G-3hp88mo?si=sTJTSzubdb0Vizjr",
"type": "video"
}
]
},
"HGmeYvRf7_XusZl_K4x9k": {
"title": "Editing Files",
"description": "Linux, like other operating systems, allows file editing for numerous purposes, whether you need to configure some system functionality or writing scripts. There's a variety of text editors available in Linux by default, these include: `nano`, `vi/vim`, `emacs`, and `gedit`. Each of these has its own learning curve and set of commands.\n\nFor instance, `nano` is a basic text editor, which is easy to use and perfect for simple text file editing. `Vi/vim`, on the other hand, is more advanced and offers a wide range of features and commands.\n\nTo edit a file you first need to open it using a command like:\n\n nano [filename]\n \n\n vi [filename] or vim [filename]\n \n\n gedit [filename]\n \n\nVisit the following resources to learn more:",
"links": [
{
"title": "How to edit a file",
"url": "https://www.scaler.com/topics/how-to-edit-a-file-in-linux/",
"type": "article"
}
]
},
"8QBMyL8D5jPovxN8jyZW9": {
"title": "Shell and Other Basics",
"description": "The Linux shell is a command-line interface that acts as an intermediary between users and the system kernel. Common shells include Bash, sh, and csh. Basic operations involve navigating directories, creating/deleting files, and executing commands. Shell knowledge is fundamental for Linux administration, scripting, and automation tasks.\n\nThis is a follow up exercise to make your first bash script. Please run the commands in the terminal one by one and try to understand what they do:\n\n touch my_first_script.sh\n chmod +x my_first_script.sh\n echo \"date\" > my_first_script.sh\n ./my_first_script.sh",
"links": []
},
"XiZz7EFIey1XKS292GN4t": {
"title": "Vim",
"description": "Vim (Vi Improved) is a powerful and flexible text editor used in Unix-like systems. It builds on the original Vi editor with additional features and improvements, including multi-level undo, syntax highlighting, and an extensive set of commands for text manipulation.\n\nVim operates primarily in three modes:\n\n* Normal (for navigation and manipulation).\n* Insert (for editing text).\n* Command (for executing commands).\n\nA simple use of Vim to edit a 'example.txt' file would look like this:\n\n vim example.txt\n \n\nTo insert new content, press 'i' for 'insert mode'. After editing, press 'ESC' to go back to 'command mode', and type ':wq' to save and quit.\n\nTo learn more, visit this:\n\nCheck out this [Github repo](https://github.com/iggredible/Learn-Vim?tab=readme-ov-file) on Vim from basic to advanced.",
"links": [
{
"title": "Learn Vimscript The Hard Way",
"url": "https://learnvimscriptthehardway.stevelosh.com/",
"type": "course"
},
{
"title": "Learn Vim Progressively",
"url": "https://yannesposito.com/Scratch/en/blog/Learn-Vim-Progressively/",
"type": "article"
},
{
"title": "Platform to practice Vim",
"url": "https://vim-adventures.com/",
"type": "article"
},
{
"title": "Vim Cheat Sheet",
"url": "https://vim.rtorr.com/",
"type": "article"
},
{
"title": "Vim basics",
"url": "https://www.youtube.com/watch?v=wACD8WEnImo&list=PLT98CRl2KxKHy4A5N70jMRYAROzzC2a6x&ab_channel=LearnLinuxTV",
"type": "video"
}
]
},
"yqRwmcZThjQuqh2ao0dWK": {
"title": "Nano",
"description": "Nano is a popular, user-friendly text editor used for creating and editing files directly within the Linux command line interface (CLI). It is an alternative to editors like `Vi` and `Emacs` and is considered more straightforward for beginners due to its simple and intuitive interface.\n\nNano comes pre-installed with many Linux distributions but if it's not installed, here's how to do it for popular Linux distributions.\n\n # Ubuntu based distributions\n sudo apt update\n sudo apt install nano\n \n\n # Arch Linux \n sudo pacman -S nano\n \n\nTo use Nano to edit or create files in Linux, the following command can be used:\n\n nano filename\n \n\nVisit the following resources to learn more:",
"links": [
{
"title": "Blog on nano",
"url": "https://ioflood.com/blog/nano-linux-command/",
"type": "article"
},
{
"title": "Nano editor fundamentals",
"url": "https://www.youtube.com/watch?v=gyKiDczLIZ4&ab_channel=HackerSploit",
"type": "video"
}
]
},
"moGMHNR58wFlzhS7je1wc": {
"title": "Command Path",
"description": "In Linux, the command path is an important concept under shell basics. Simply put, command path is a variable that is used by the shell to determine where to look for the executable files to run. Linux commands are nothing but programs residing in particular directories. But, one does not have to navigate to these directories every time to run these programs. The command path comes to the rescue!\n\nUsually, when you type a command in the terminal, the shell needs to know the absolute path of the command's executable to run it. Instead of typing the full path each time, command paths allow the shell to automatically search the indicated directories in the correct order. These paths are stored in the $PATH environment variable.\n\n echo $PATH\n \n\nRunning this command in a Linux terminal will return all the directories that the shell will search, in order, to find the command it has to run. The directories are separated by a colon.\n\nThis feature makes using Linux command-line interface convenient and efficient.",
"links": []
},
"zwXEmpPYjA7_msS43z7I0": {
"title": "Environment Variables",
"description": "In Linux, environment variables are dynamic named values that can affect the behavior of running processes in a shell. They exist in every shell session. A shell session's environment includes, but is not limited to, the user's home directory, command search path, terminal type, and program preferences.\n\nEnvironment variables help to contribute to the fantastic and customizable flexibility you see in Unix systems. They provide a simple way to share configuration settings between multiple applications and processes in Linux.\n\nYou can use the 'env' command to list all the environment variables in a shell session. If you want to print a particular variable, such as the PATH variable, you can use the 'echo $PATH' command.\n\nHere's an example of how you would do that:\n\n # List all environment variables\n $ env\n \n # Print a particular variable like PATH\n $ echo $PATH\n \n\nRemember, every shell, such as Bourne shell, C shell, or Korn shell in Unix or Linux has different syntax and semantics to define and use environment variables.\n\nLearn more from the following resources:",
"links": [
{
"title": "Environment Variables in Linux",
"url": "https://labex.io/tutorials/linux-environment-variables-in-linux-385274",
"type": "article"
}
]
},
"KaMSsQnJzNqGHg0Oia4uy": {
"title": "Command Help",
"description": "Linux command help provides documentation and usage information for shell commands. Use `man command` for detailed manuals, `help command` for shell built-ins, `command --help` for quick options, and `tldr command` for practical examples. Essential for learning command syntax, parameters, and functionality in Linux terminal environments.\n\nLearn more from the following resources:",
"links": [
{
"title": "tldr-pages/tldr",
"url": "https://github.com/tldr-pages/tldr",
"type": "opensource"
},
{
"title": "How to use the man page",
"url": "https://www.baeldung.com/linux/man-command",
"type": "article"
},
{
"title": "Get Help on Linux Commands",
"url": "https://labex.io/tutorials/linux-get-help-on-linux-commands-18000",
"type": "article"
}
]
},
"JgoZzx4BfK7tmosgpZOsf": {
"title": "Redirects",
"description": "The shell in Linux provides a robust way of managing input and output streams of a command or program, this mechanism is known as Redirection. Linux being a multi-user and multi-tasking operating system, every process typically has 3 streams opened:\n\n* Standard Input (stdin) - This is where the process reads its input from. The default is the keyboard.\n* Standard Output (stdout) - The process writes its output to stdout. By default, this means the terminal.\n* Standard Error (stderr) - The process writes error messages to stderr. This also goes to the terminal by default.\n\nRedirection in Linux allows us to manipulate these streams, advancing the flexibility with which commands or programs are run. Besides the default devices (keyboard for input and terminal for output), the I/O streams can be redirected to files or other devices.\n\nFor example, if you want to store the output of a command into a file instead of printing it to the console, we can use the '>' operator.\n\n ls -al > file_list.txt\n \n\nThis command will write the output of 'ls -al' into 'file\\_list.txt', whether or not the file initially existed. It will be created if necessary, and if it already exists it will be overwritten.\n\nLearn more from the following resources:",
"links": [
{
"title": "Logical Commands and Redirection",
"url": "https://labex.io/tutorials/linux-logical-commands-and-redirection-387332",
"type": "article"
}
]
},
"NIBSZGE9PskVrluJpdom0": {
"title": "Super User",
"description": "The Super User, also known as \"root user\", represents a user account in Linux with extensive powers, privileges, and capabilities. This user has complete control over the system and can access any data stored on it. This includes the ability to modify system configurations, change other user's passwords, install software, and perform more administrative tasks in the shell environment.\n\nThe usage of super user is critical to operating a Linux system properly and safely as it can potentially cause serious damage. The super user can be accessed through the `sudo` or `su` commands.\n\nSpecifically, `su` switches the current user to the root, whereas `sudo` allows you to run a command as another user, default being root. However, they also have a key difference which is `sudo` will log the commands and its arguments which can be a handy audit trail.\n\n # This would prompt for root password and switch you to root usermode\n $ su -\n \n # To perform a command as superuser (if allowed in sudoers list)\n $ sudo <command>\n \n\nNote that super user privileges should be handled with care due to their potential to disrupt the system's functionality. Mistaken changes to key system files or unauthorized access can lead to severe issues.",
"links": []
},
"RsOTPZPZGTEIt1Lk41bQV": {
"title": "Working with Files",
"description": "Working with files is an essential part of Linux and it's a skill every Linux user must have. In Linux, everything is considered a file: texts, images, systems, devices, and directories. Linux provides multiple command-line utilities to create, view, move or search files. Some of the basic commands for file handling in Linux terminal include `touch` for creating files, `mv` for moving files, `cp` for copying files, `rm` for removing files, and `ls` for listing files and directories.\n\nFor instance, to create a file named \"example.txt\", we use the command:\n\n touch example.txt\n \n\nTo list files in the current directory, we use the command:\n\n ls\n \n\nKnowing how to effectively manage and manipulate files in Linux is crucial for administering and running a successful Linux machine.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux Basic Files Operations",
"url": "https://labex.io/tutorials/linux-basic-files-operations-270248",
"type": "article"
}
]
},
"TnrT-cqMA8urew9nLv0Ns": {
"title": "File Permissions",
"description": "Linux file permissions control read (r), write (w), and execute (x) access for owner, group, and others using octal or symbolic notation. Format `-rwxr--r--` shows file type and permissions. Use `chmod` to change permissions, `chown` for ownership, `chgrp` for group ownership. Essential for system security and proper access control.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux File Permissions",
"url": "https://linuxhandbook.com/linux-file-permissions/",
"type": "article"
},
{
"title": "Linux Permissions of Files",
"url": "https://labex.io/tutorials/linux-permissions-of-files-270252",
"type": "article"
},
{
"title": "Linux File Permissions in 5 Minutes",
"url": "https://www.youtube.com/watch?v=LnKoncbQBsM",
"type": "video"
}
]
},
"iD073xTmpzvQFfXwcwXcY": {
"title": "Archiving and Compressing",
"description": "Linux archiving combines multiple files into single archives using `tar`, while compression reduces file sizes with `gzip` and `bzip2`. Use `tar cvf` to create archives, `tar xvf` to extract, and `tar cvzf` for gzip-compressed archives. These separate processes are often combined for efficient backup and distribution, with `tar.gz` and `tar.bz2` being common formats.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux File Packaging and Compression",
"url": "https://labex.io/tutorials/linux-file-packaging-and-compression-385413",
"type": "article"
}
]
},
"abKO6KuuIfl9ruVxBw6t_": {
"title": "Copying and Renaming",
"description": "Essential Linux file operations use `cp` to copy files and `mv` to move/rename them. The `cp` command copies files from source to destination, while `mv` moves or renames files/directories. Both commands use the syntax `command source destination`. These case-sensitive commands are fundamental for daily file management tasks in Linux systems.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux cp Command: File Copying",
"url": "https://labex.io/tutorials/linux-linux-cp-command-file-copying-209744",
"type": "article"
},
{
"title": "Linux mv Command: File Moving and Renaming",
"url": "https://labex.io/tutorials/linux-linux-mv-command-file-moving-and-renaming-209743",
"type": "article"
}
]
},
"KaXHG_EKxI5PUXmcvlJt6": {
"title": "Soft Links / Hard Links",
"description": "Linux supports two types of file links. Hard links share the same inode and data as the original file - if the original is deleted, data remains accessible. Soft links (symbolic links) are shortcuts pointing to the original file path - they break if the original is removed. Create with `ln` for hard links and `ln -s` for soft links.\n\nBelow is an example of how to create a soft link and a hard link in Linux:\n\n # Create a hard link\n ln source_file.txt hard_link.txt\n \n # Create a soft link\n ln -s source_file.txt soft_link.txt\n \n\nPlease, understand that `source_file.txt` is the original file and `hard_link.txt` & `soft_link.txt` are the hard and soft links respectively.\n\nLearn more from the following resources:",
"links": [
{
"title": "How to understand the difference between hard and symbolic links in Linux",
"url": "https://labex.io/tutorials/linux-how-to-understand-the-difference-between-hard-and-symbolic-links-in-linux-409929",
"type": "article"
}
]
},
"-B2Dvz7160Er0OBHzS6ro": {
"title": "Text Processing",
"description": "Text processing is an essential task for system administrators and developers. Linux, being a robust operating system, provides powerful tools for text searching, manipulation, and processing.\n\nUsers can utilize commands like `awk`, `sed`, `grep`, and `cut` for text filtering, substitution, and handling regular expressions. Additionally, the shell scripting and programming languages such as Python and Perl also provide remarkable text processing capabilities in Linux.\n\nAlthough being primarily a command-line operating system, Linux also offers numerous GUI-based text editors including `gedit`, `nano`, and `vim`, which make text editing convenient for both beginners and advanced users.\n\nBelow is a simple example using `grep` command to search for the term \"Linux\" in a file named \"sample.txt\".\n\n grep 'Linux' sample.txt\n \n\nThis command will display all the lines in the sample.txt file which contain the word \"Linux\".\n\nOverall, the proficiency in text processing is crucial for Linux users as it allows them to automate tasks, parse files, and mine data efficiently.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Linux Filters",
"url": "https://ryanstutorials.net/linuxtutorial/filters.php",
"type": "article"
},
{
"title": "Linux Text Processing Command",
"url": "https://earthly.dev/blog/linux-text-processing-commands/",
"type": "article"
},
{
"title": "Master Linux Text Processing Commands",
"url": "https://everythingdevops.dev/linux-text-processing-commands/",
"type": "article"
}
]
},
"t3fxSgCgtxuMtHjclPHA6": {
"title": "stdout / stdin / stderr",
"description": "Linux processes use three standard data streams: STDIN (input), STDOUT (output), and STDERR (error messages). STDOUT handles normal command output while STDERR specifically handles error messages. You can redirect these streams using operators like `>` for stdout and `2>` for stderr, allowing separate handling of normal output and errors for better scripting and debugging.\n\nHere is an example code snippet showing how these channels are used:\n\n $ command > stdout.txt 2>stderr.txt\n \n\nIn this example, the \">\" operator redirects the standard output (stdout) into a text file named stdout.txt, while \"2>\" redirects the standard error (stderr) into stderr.txt. This way, normal output and error messages are separately stored in distinct files for further examination or processing.",
"links": []
},
"Z5Mf_e5G24IkmxEHgYBe2": {
"title": "cut",
"description": "The `cut` command is a text processing utility that allows you to cut out sections of each line from a file or output, and display it on the standard output (usually, the terminal). It's commonly used in scripts and pipelines, especially for file operations and text manipulation.\n\nThis command is extremely helpful when you only need certain parts of the file, such as a column, a range of columns, or a specific field. For example, with Linux system logs or CSV files, you might only be interested in certain bits of information.\n\nA basic syntax of `cut` command is:\n\n cut OPTION... [FILE]...\n \n\nHere's an example of how you might use the `cut` command in Linux:\n\n echo \"one,two,three,four\" | cut -d \",\" -f 2\n \n\nThis command will output the second field (`two`) by using the comma as a field delimiter (`-d \",\"`).\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux cut Command: Text Cutting",
"url": "https://labex.io/tutorials/linux-linux-cut-command-text-cutting-219187",
"type": "article"
}
]
},
"5658kdqJw-pIOyyhll80a": {
"title": "paste",
"description": "In Linux, paste is a powerful text processing utility that is primarily used for merging lines from multiple files. It allows users to combine data by columns rather than rows, adding immense flexibility to textual data manipulation. Users can choose a specific delimiter for separating columns, providing a range of ways to format the output.\n\nA common use case of the paste command in Linux is the combination of two text files into one, like shown in the example snippet below.\n\n paste file1.txt file2.txt > combined.txt\n \n\nOver the years, this command has proved to be critical in Linux file processing tasks due to its efficiency, and simplicity.",
"links": []
},
"1WRIy3xHtQfiQFZrprobP": {
"title": "sort",
"description": "Linux provides a variety of tools for processing and manipulating text files, one of which is the sort command. The `sort` command in Linux is used to sort the contents of a text file, line by line. The command uses ASCII values to sort files. You can use this command to sort the data in a file in a number of different ways such as alphabetically, numerically, reverse order, or even monthly. The sort command takes a file as input and prints the sorted content on the standard output (screen).\n\nHere is a basic usage of the `sort` command:\n\n sort filename.txt\n \n\nThis command prints the sorted content of the filename.txt file. The original file content remains unchanged. In order to save the sorted contents back into the file, you can use redirection:\n\n sort filename.txt > sorted_filename.txt\n \n\nThis command sorts the content of filename.txt and redirects the sorted content into sorted\\_filename.txt.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux sort Command: Text Sorting",
"url": "https://labex.io/tutorials/linux-linux-sort-command-text-sorting-219196",
"type": "article"
}
]
},
"6xdkFk_GT93MigeTSSGCp": {
"title": "head",
"description": "The `head` command in Linux is a text processing utility that allows a user to output the first part (or the \"head\") of files. It is commonly used for previewing the start of a file without loading the entire document into memory, which can act as an efficient way of quickly examining the data in very large files. By default, the `head` command prints the first 10 lines of each file to standard output, which is the terminal in most systems.\n\n head file.txt\n \n\nThe number of output lines can be customized using an option. For example, to display first 5 lines, we use `-n` option followed by the number of lines:\n\n head -n 5 file.txt\n \n\nLearn more from the following resources:",
"links": [
{
"title": "Linux head Command: File Beginning Display",
"url": "https://labex.io/tutorials/linux-linux-head-command-file-beginning-display-214302",
"type": "article"
}
]
},
"O9Vci_WpUY-79AkA4HDx3": {
"title": "tr",
"description": "The `tr` command in Linux is a command-line utility that translates or substitutes characters. It reads from the standard input and writes to the standard output. Although commonly used for translation applications, `tr` has versatile functionality in the text processing aspect of Linux. Ranging from replacing a list of characters, to deleting or squeezing character repetitions, `tr` presents a robust tool for stream-based text manipulations.\n\nHere's a basic usage example:\n\n echo 'hello' | tr 'a-z' 'A-Z'\n \n\nIn this example, `tr` is used to convert the lowercase 'hello' to uppercase 'HELLO'. It's an essential tool for text processing tasks in the Linux environment.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux tr Command: Character Translating",
"url": "https://labex.io/tutorials/linux-linux-tr-command-character-translating-388064",
"type": "article"
}
]
},
"Yyk28H6TiteZEGv6Aps1h": {
"title": "tail",
"description": "The `tail` command in Linux is a utility used in text processing. Fundamentally, it's used to output the last part of the files. The command reads data from standard input or from a file and outputs the last `N` bytes, lines, blocks, characters or words to the standard output (or a different file). By default, `tail` returns the last 10 lines of each file to the standard output. This command is common in situations where the user is interested in the most recent entries in a text file, such as log files.\n\nHere is an example of tail command usage:\n\n tail /var/log/syslog\n \n\nIn the above example, the `tail` command will print the last 10 lines of the `/var/log/syslog` file. This is particularly useful in checking the most recent system log entries.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux tail Command: File End Display",
"url": "https://labex.io/tutorials/linux-linux-tail-command-file-end-display-214303",
"type": "article"
}
]
},
"vfcCS1GoyKpU1rQaE8I5r": {
"title": "join",
"description": "`join` is a powerful text processing command in Linux. It lets you combine lines of two files on a common field, which works similar to the 'Join' operation in SQL. It's particularly useful when you're dealing with large volumes of data. Specifically, `join` uses the lines from two files to form lines that contain pairs of lines related in a meaningful way.\n\nFor instance, if you have two files that have a list of items, one with costs and the other with quantities, you can use `join` to combine these two files so each item has a cost and quantity on the same line.\n\n # Syntax\n join file1.txt file2.txt\n \n\nPlease note that `join` command works properly only when the files are sorted. It's crucial to understand all the provided options and flags to use `join` effectively in text processing tasks.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux join Command: File Joining",
"url": "https://labex.io/tutorials/linux-linux-join-command-file-joining-219193",
"type": "article"
}
]
},
"Pl9s2ti25hsSEljXJvBTj": {
"title": "split",
"description": "Linux provides an extensive set of tools for manipulating text data. One of such utilities is the `split` command that is used, as the name suggests, to split large files into smaller files. The `split` command in Linux divides a file into multiple equal parts, based on the lines or bytes specified by the user.\n\nIt's a useful command because of its practical applicability. For instance, if you have a large data file that can't be used efficiently because of its size, then the split command can be used to break up the file into more manageable pieces.\n\nThe basic syntax of the `split` command is:\n\n split [options] [input [prefix]]\n \n\nBy default, the `split` command divides the file into smaller files of 1000 lines each. If no input file is provided, or if it is given as -, it reads from standard input.\n\nFor example, to split a file named 'bigfile.txt' into files of 500 lines each, the command would be:\n\n split -l 500 bigfile.txt",
"links": []
},
"v32PJl4fzIFTOirOm6G44": {
"title": "pipe",
"description": "The pipe (`|`) is a powerful feature in Linux used to connect two or more commands together. This mechanism allows output of one command to be \"piped\" as input to another. With regards to text processing, using pipe is especially helpful since it allows you to manipulate, analyze, and transform text data without the need to create intermediary files or programs.\n\nHere is a simple example of piping two commands, `ls` and `grep`, to list all the text files in the current directory:\n\n ls | grep '\\.txt$'\n \n\nIn this example, `ls` lists the files in the current directory and `grep '\\.txt$'` filters out any files that don't end with `.txt`. The pipe command, `|`, takes the output from `ls` and uses it as the input to `grep '\\.txt$'`. The output of the entire command is the list of text files in the current directory.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Piping and Redirection",
"url": "https://ryanstutorials.net/linuxtutorial/piping.php#piping",
"type": "article"
}
]
},
"Bo9CdrGJej-QcNmw46k9t": {
"title": "tee",
"description": "The `tee` command reads from standard input and writes to both standard output and files simultaneously, like a T-splitter in plumbing. It enables users to view results in the terminal while saving output to files concurrently. Syntax: `command | tee file`. Extremely useful for documenting terminal activities and preserving command outputs for later analysis.",
"links": []
},
"YSfGrmT795miIeIZrtC3D": {
"title": "nl",
"description": "The `nl` command numbers lines in text files, providing an overview of line locations. By default, it numbers only non-empty lines, but this behavior can be modified. Syntax: `nl [options] [file_name]`. If no file is specified, nl reads from stdin. Valuable for text processing when line numbers are needed for reference or debugging purposes.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux nl Command: Line Numbering",
"url": "https://labex.io/tutorials/linux-linux-nl-command-line-numbering-210988",
"type": "article"
}
]
},
"LIGOJwrXexnIcPyHVlhQ8": {
"title": "wc",
"description": "The `wc` command is a commonly used tool in Unix or Linux that allows users to count the number of bytes, characters, words, and lines in a file or in data piped from standard input. The name `wc` stands for 'word count', but it can do much more than just count words. Common usage of `wc` includes tracking program output, counting code lines, and more. It's an invaluable tool for analyzing text at both granular and larger scales.\n\nBelow is a basic usage example for `wc` in Linux:\n\n wc myfile.txt\n \n\nThis command would output the number of lines, words, and characters in `myfile.txt`. The output is displayed in the following order: line count, word count, character count, followed by the filename.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux wc Command: Text Counting",
"url": "https://labex.io/tutorials/linux-linux-wc-command-text-counting-219200",
"type": "article"
}
]
},
"TZuDVFS7DZFBgaSYYXoGe": {
"title": "expand",
"description": "The `expand` command converts tabs to spaces in text files, useful for consistent formatting across different systems and editors. Default conversion is 8 spaces per tab. Use `expand filename` for basic conversion or `expand -t 4 filename` to specify 4 spaces per tab. Essential for maintaining code readability and consistent indentation in shell scripts.",
"links": []
},
"sKduFaX6xZaUUBdXRMKCL": {
"title": "unexpand",
"description": "The `unexpand` command converts spaces to tabs in text files, making documents more coherent and neat. Commonly used in programming scripts where tab indentation is preferred. Use `unexpand -t 4 file.txt` to replace every four spaces with a tab. Opposite of `expand` command, useful for standardizing indentation formatting in code files.\n\nAn example of using the `unexpand` command:\n\n unexpand -t 4 file.txt",
"links": []
},
"qnBbzphImflQbEbtFub9x": {
"title": "uniq",
"description": "In Linux, `uniq` is an extremely useful command-line program for text processing. It aids in the examination and manipulation of text files by comparing or filtering out repeated lines that are adjacent. Whether you're dealing with a list of data or a large text document, the `uniq` command allows you to find and filter out duplicate lines, or even provide a count of each unique line in a file. It's important to remember that `uniq` only removes duplicates that are next to each other, so to get the most out of this command, data is often sorted using the `sort` command first.\n\nAn example of using `uniq` would be:\n\n sort names.txt | uniq\n \n\nIn this example, `names.txt` is a file containing a list of names. The `sort` command sorts all the lines in the file, and then the `uniq` command removes all the duplicate lines. The resulting output would be a list of unique names from `names.txt`.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux uniq Command: Duplicate Filtering",
"url": "https://labex.io/tutorials/linux-linux-uniq-command-duplicate-filtering-219199",
"type": "article"
}
]
},
"umlhxidsvtZG9k40Ca0Ac": {
"title": "grep",
"description": "GREP (Global Regular Expression Print) is a powerful text search utility that finds and filters text matching specific patterns in files. It searches line by line and prints matching lines to the screen. Essential for shell scripts and command-line operations. Example: `grep \"pattern\" fileName` searches for specified patterns. Alternative: `ripgrep` offers enhanced performance and features.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Ripgrep: Github Repository",
"url": "https://github.com/BurntSushi/ripgrep",
"type": "opensource"
},
{
"title": "Grep and Regular Expressions for Beginners",
"url": "https://ryanstutorials.net/linuxtutorial/grep.php",
"type": "article"
},
{
"title": "bgsu.edu: Advanced Grep Topics",
"url": "https://caspar.bgsu.edu/~courses/Stats/Labs/Handouts/grepadvanced.htm",
"type": "article"
},
{
"title": "Linux grep Command: Pattern Searching",
"url": "https://labex.io/tutorials/linux-linux-grep-command-pattern-searching-219192",
"type": "article"
}
]
},
"QTmECqpRVMjNgQU70uCF8": {
"title": "awk",
"description": "AWK is a powerful text-processing language for Unix-like systems, named after its creators Aho, Weinberger, and Kernighan. It reads files line by line, identifies patterns, and executes actions on matches. Commonly used in bash scripts for sorting, filtering, and report generation. Example: `awk '{print $1,$2}' filename` prints first two fields of each line.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "IBM.com: Awk by Example",
"url": "https://developer.ibm.com/tutorials/l-awk1/",
"type": "article"
},
{
"title": "Linux Handbook: Awk",
"url": "https://linuxhandbook.com/awk-command-tutorial/",
"type": "article"
},
{
"title": "Explore top posts about Bash",
"url": "https://app.daily.dev/tags/bash?ref=roadmapsh",
"type": "article"
},
{
"title": "Linux awk Command: Text Processing",
"url": "https://labex.io/tutorials/linux-linux-awk-command-text-processing-388493",
"type": "article"
},
{
"title": "YouTube",
"url": "https://www.youtube.com/watch?v=9YOZmI-zWok",
"type": "video"
}
]
},
"jSzfQf0MlnXtWHCc-HYvr": {
"title": "Server Review",
"description": "Server review in Linux involves assessing performance, security, and configuration to identify improvements and issues. Check security enhancements, log files, user accounts, network configuration, and software versions. Common commands: `free -m` for memory, `df -h` for disk usage, `uptime` for CPU load. Critical task for system administrators and DevOps professionals to ensure optimal performance, security, and reliability.\n\nLinux, known for its stability and security, has become a staple on the back-end of many networks and servers worldwide. Depending on the distribution you are using, Linux offers multiple tools and commands to perform comprehensive server reviews.\n\n # A command often used for showing memory information\n free -m\n \n # A command for showing disk usage\n df -h\n \n # A command for showing CPU load\n uptime",
"links": []
},
"19lTWqAvZFT2CDlhLlPSq": {
"title": "Uptime and Load",
"description": "The `uptime` command shows how long a Linux system has been running and the system load average. Load average indicates computational work and CPU queue length, displayed for 1, 5, and 15-minute intervals. High load averages suggest resource constraints or performance issues. Regular monitoring helps identify usage patterns and plan capacity.\n\nHere is an example of the `uptime` command and its output:\n\n $ uptime\n 10:58:35 up 2 days, 20 min, 1 user, load average: 0.00, 0.01, 0.05\n \n\nIn the output above, \"2 days, 20 min\" tells us how long the system has been up, while \"0.00, 0.01, 0.05\" shows the system's load average over the last one, five, and fifteen minutes, respectively.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux Load Average: What is Load Average in Linux?",
"url": "https://www.digitalocean.com/community/tutorials/load-average-in-linux",
"type": "article"
}
]
},
"WwybfdKuP9ogCGpT7d3NU": {
"title": "Authentication Logs",
"description": "Authentication logs in Linux record all auth-related events like logins, password changes, and sudo commands. Located at `/var/log/auth.log` (Debian) or `/var/log/secure` (RHEL/CentOS), these logs help detect brute force attacks and unauthorized access attempts. Use `tail /var/log/auth.log` to view recent entries. Regular log analysis is essential for server security monitoring.\n\nHere is an example of how you can use the `tail` command to view the last few entries of the authentication log:\n\n tail /var/log/auth.log\n \n\nGet yourself familiar with reading and understanding auth logs, as it's one essential way to keep your server secure.",
"links": []
},
"ewUuI_x-YhOQIYd3MTgJJ": {
"title": "Services Running",
"description": "Linux servers run various services including web, database, DNS, and mail servers. System administrators use tools like `systemctl`, `service`, `netstat`, `ss`, and `lsof` to manage and monitor services. Use `systemctl --type=service` to list all active services with their status. Essential for server management, resource monitoring, and troubleshooting.\n\nLinux has a variety of tools to achieve this, such as: `systemctl`, `service`, `netstat`, `ss` and \\`",
"links": []
},
"tx0nh6cbBjVxwNlyrBNYm": {
"title": "Available Memory / Disk",
"description": "Linux provides tools like `free`, `vmstat`, and `top` to monitor system memory usage and performance. The `free -h` command shows total, used, free, shared, buffer/cache, and available memory in human-readable format. Regular memory monitoring helps maintain optimal server performance, prevent overload, and troubleshoot resource issues effectively.\n\nThe `free` command, for instance, gives a summary of the overall memory usage including total used and free memory, swap memory and buffer/cache memory. Here's an example:\n\n $ free -h\n total used free shared buff/cache available\n Mem: 15Gi 10Gi 256Mi 690Mi 5.3Gi 4.2Gi\n Swap: 8.0Gi 1.3Gi 6.7Gi\n \n\nIn this output, the '-h' option is used to present the results in a human-readable format. Understanding the state of memory usage in your Linux server can help maintain optimal server performance and troubleshoot any potential issues.",
"links": []
},
"h01Y6dW09ChidlM2HYoav": {
"title": "Process Management",
"description": "Linux treats every running program as a process. Process management commands help view, control, and manipulate these processes. Key commands: `ps aux` shows running processes, `top` provides live system view, `kill -SIGTERM pid` gracefully stops processes, `kill -SIGKILL pid` forcefully terminates processes. Essential for understanding and controlling Linux system operations effectively.",
"links": []
},
"mUKoiGUTpIaUgQNF3BND_": {
"title": "Background / Foreground Processes",
"description": "Linux processes run in foreground (fg) taking direct user input or background (bg) running independently. Start background processes with `command &` or use `Ctrl+Z` then `bg` to pause and resume in background. Use `fg` to bring background processes to foreground. These job control commands enable managing multiple tasks from a single terminal efficiently.\n\nHere's how you can send a running process to background:\n\n command &\n \n\nOr if a process is already running:\n\n CTRL + Z # This will pause the process\n bg # This resumes the paused process in the background\n \n\nAnd to bring it back to the foreground:\n\n fg\n \n\nThese commands, `bg` and `fg` are part of job control in Unix-like operating systems, which lets you manage multiple tasks simultaneously from a single terminal.",
"links": []
},
"lf3_CRyOI2ZXGzz5ff451": {
"title": "Listing / Finding Processes",
"description": "Linux provides several tools to list and monitor running processes. The `/proc` filesystem contains process information accessible via PID directories. Commands like `ps -ef` show process snapshots, while `top` and `htop` provide real-time process monitoring. Use `cat /proc/{PID}/status` to view specific process details. These tools are essential for system monitoring and troubleshooting.\n\n # list all running processes\n ps -ef \n \n # display ongoing list of running processes \n top\n \n # alternatively, for a more user-friendly interface\n htop\n \n\nExploring the proc directory (`/proc`), we dive even deeper, enabling us to view the system's kernel parameters and each process's specific system details.\n\n # view specifics of a particular PID\n cat /proc/{PID}/status\n \n\nIn short, 'Finding and Listing Processes (proc)' in Linux is not just a core aspect of process management, but also a necessary skill for enhancing system performance and resolution of issues.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "The /proc File System",
"url": "https://www.kernel.org/doc/html/latest/filesystems/proc.html",
"type": "article"
}
]
},
"VkLWTvKnRXzvLGWza2v45": {
"title": "Process Signals",
"description": "Process signals are communication mechanisms in Linux that notify processes of synchronous or asynchronous events. Common signals include SIGINT, SIGSTOP, SIGKILL for interrupting, pausing, or terminating processes. Use the `kill` command to send signals to processes by PID. Understanding signals is essential for effective process management and system control.\n\nFor instance, to send a SIGSTOP signal to a process with a PID of 12345 you would use `kill` command in terminal as follows:\n\n kill -SIGSTOP 12345\n \n\nThis will suspend the execution of the process until a SIGCONT signal is received.\n\nUnderstanding proc signals is essential for comprehensive process management and resource allocation in Linux.",
"links": []
},
"0FLUI9r7znMqi6YKReLzD": {
"title": "Killing Processes",
"description": "The `kill` command terminates processes in Linux by sending signals to specific Process IDs (PIDs). Use `kill [signal] PID` to terminate processes manually. Different signals provide various termination methods - SIGTERM for graceful shutdown, SIGKILL for forced termination. Process termination is essential for managing unresponsive or unwanted processes.\n\n'Kill' in Linux is a built-in command that is used to terminate processes manually. You can use the `kill` command to send a specific signal to a process. When we use the `kill` command, we basically request a process to stop, pause, or terminate.\n\nHere's a basic illustration on how to use the `kill` command in Linux:\n\n kill [signal or option] PID(s)\n \n\nIn practice, you would identify the Process ID (PID) of the process you want to terminate and replace PID(s) in the above command. The signal or option part is optional, but very powerful allowing for specific termination actions.",
"links": []
},
"5anSYRhaKIs3dCLWlvZfT": {
"title": "Process Priorities",
"description": "Linux assigns priority levels to processes, affecting execution timing and resource allocation. Process priorities use \"nice\" values ranging from -20 (highest priority) to +19 (lowest priority). The `/proc` filesystem contains process information including priorities. You can view priorities with `ps -eo pid,pri,user,comm` and modify them using `renice` command.\n\nHere's a simple command in the Linux terminal to display the process ID, priority, and user for all processes:",
"links": []
},
"Rib7h9lh_ndiXkwNbftz_": {
"title": "Process Forking",
"description": "Process forking allows a running process (parent) to create a copy of itself (child) using the `fork()` system call. The child process is nearly identical to the parent except for process ID and parent process ID. Both processes execute concurrently and independently - changes in one don't affect the other. This mechanism is fundamental for Linux process creation.\n\nHere's a basic code snippet of proc forking in C:\n\n #include<sys/types.h>\n #include<unistd.h>\n #include<stdio.h>\n \n int main()\n {\n pid_t child_pid;\n \n // Try creating a child process\n child_pid = fork();\n \n // If a child is successfully created\n if(child_pid >= 0)\n printf(\"Child created with PID: %d\\n\", child_pid);\n else\n printf(\"Fork failed\\n\");\n return 0;\n }\n \n\nIn this snippet, `fork()` is used to created a new child process. If the process creation is successful, fork() returns the process ID of the child process. If unsuccessful, it returns a negative value.",
"links": []
},
"g6n7f1Qi0BPr_BGvisWuz": {
"title": "User Management",
"description": "Linux user management allows multiple users to interact with the system in isolation. Includes creating, deleting, modifying users and groups, assigning permissions and ownership. Key commands: `adduser`/`useradd` creates users, `deluser`/`userdel` removes users, `passwd` manages passwords, `su` switches users. Essential for providing proper accessibility and maintaining Linux system security.\n\nLearn more from the following resources:",
"links": [
{
"title": "User Account Management",
"url": "https://labex.io/tutorials/linux-user-account-management-49",
"type": "article"
}
]
},
"R9TZfkgVUQNLnMpDhovJa": {
"title": "Create / Delete / Update",
"description": "Linux user management involves creating, updating, and deleting user accounts for system security and resource management. Use `useradd` or `adduser` to create users, `usermod` to update user details like home directory or shell, and `userdel` to delete users. Effective user management maintains system security and organization in multi-user environments.\n\nLearn more from the following resources:",
"links": [
{
"title": "How to create, update, and delete users account on Linux",
"url": "https://linuxconfig.org/how-to-create-modify-and-delete-users-account-on-linux",
"type": "article"
},
{
"title": "How to manage users in Linux",
"url": "https://www.freecodecamp.org/news/how-to-manage-users-in-linux/",
"type": "article"
}
]
},
"h8wc8XEwWYHErna68w7Mg": {
"title": "Users and Groups",
"description": "User management in Linux uses groups to organize users and manage permissions efficiently. Groups are collections of users that simplify system administration by controlling access to resources like files and directories. Users can belong to multiple groups, enabling precise privilege management. Commands like `groupadd`, `groupdel`, `groupmod`, `usermod`, and `gpasswd` manage groups effectively. Proper group management is crucial for a secure and organized system environment. For detailed instructions, refer to resources on managing Linux groups.\n\nLearn more from the following resources:",
"links": [
{
"title": "How to create, delete, and modify groups in Linux",
"url": "https://www.redhat.com/sysadmin/linux-groups",
"type": "article"
},
{
"title": "How to manage groups on Linux",
"url": "https://linuxconfig.org/how-to-manage-groups-on-linux",
"type": "article"
}
]
},
"L6RMExeqi9501y-eCHDt1": {
"title": "Managing Permissions",
"description": "Linux file permissions control access to files and directories using read, write, and execute rights for user, group, and others. Use `chmod` to change permissions, `chown` to change ownership, and `chgrp` to change group ownership. Proper permission management is essential for system security and prevents unauthorized access to sensitive files and directories.\n\nLearn more from the following resources:",
"links": [
{
"title": "Linux file permissions explained",
"url": "https://www.redhat.com/sysadmin/linux-file-permissions-explained",
"type": "article"
},
{
"title": "Linux file permissions in 5 minutes",
"url": "https://www.youtube.com/watch?v=LnKoncbQBsM",
"type": "video"
}
]
},
"F1sU3O1ouxTOvpidDfN3k": {
"title": "Service Management (systemd)",
"description": "Service management in Linux controls system daemons during boot/shutdown processes. Modern Linux distributions use systemd for service management with commands like `systemctl start/stop/restart/status/enable/disable`. Services perform various background functions independent of user interfaces. Effective service management is essential for system stability and security.\n\nLearn more from the following resources:",
"links": [
{
"title": "How to Master Linux Service Management with Systemctl",
"url": "https://labex.io/tutorials/linux-how-to-master-linux-service-management-with-systemctl-392864",
"type": "article"
}
]
},
"34UUrc8Yjc_8lvTL8itc3": {
"title": "Creating New Services",
"description": "Creating custom services in Linux involves writing systemd service unit files that define how processes should start, stop, and restart. Service files are placed in `/etc/systemd/system/` and contain sections like \\[Unit\\], \\[Service\\], and \\[Install\\]. Use `systemctl enable` to enable services at boot and `systemctl start` to run them. Custom services allow automation of background processes.",
"links": []
},
"FStz-bftQBK0M6zz2Bxl4": {
"title": "Checking Service Logs",
"description": "Linux service management involves controlling system services like databases, web servers, and network services. Use `systemctl start service_name` to start services, `systemctl stop service_name` to stop them, and `systemctl restart service_name` to restart. These commands require root permissions via sudo and are essential for system administration and configuration management.\n\nHere is a simple example:\n\n # To start a service\n sudo systemctl start service_name \n \n # To stop a service\n sudo systemctl stop service_name \n \n # To restart a service\n sudo systemctl restart service_name \n \n\nReplace `service_name` with the name of the service you want to start, stop or restart. Always make sure to use sudo to execute these commands as they require root permissions. Please note, these commands will vary based on the specific Linux distribution and the init system it uses.",
"links": []
},
"DuEfJNrm4Jfmp8-8Pggrf": {
"title": "Starting / Stopping Services",
"description": "System logs are essential for troubleshooting and monitoring Linux systems. Most logs are stored in `/var/log` directory and managed by systemd. Use `journalctl` to view system logs and `journalctl -u service_name` for specific service logs. The `dmesg` command displays kernel messages. Regular log monitoring is crucial for system administration.\n\n journalctl\n \n\nThis command will show the entire system log from the boot to the moment you're calling the journal.\n\nTo display logs for a specific service, the `-u` option can be used followed by the service's name.\n\n journalctl -u service_name\n \n\nRemember, understanding and monitoring your system logs will provide you a clear view of what's going on in your Linux environment. It is a vital skill worth developing to effectively manage and troubleshoot systems.",
"links": []
},
"xk5Xgi797HlVjdZJRfwX1": {
"title": "Checking Service Status",
"description": "Checking service status in Linux helps monitor system health and troubleshoot issues. Use `systemctl status service_name` to view detailed service information including active state, process ID, and recent log entries. Commands like `systemctl is-active` and `systemctl is-enabled` provide quick status checks. Service status monitoring is crucial for maintaining system reliability and performance.",
"links": []
},
"4eINX8jYMJxfYh7ZV47YI": {
"title": "Package Management",
"description": "Package management handles software installation, updates, configuration, and removal in Linux. It manages collections of files and tracks software prerequisites automatically. Common package managers include `apt` (Debian-based), `yum`/`dnf` (Red Hat-based), and `pacman` (Arch). Example: `sudo apt install <package-name>` installs packages. Essential for efficient application management.\n\nLearn more from the following resources:",
"links": [
{
"title": "Software Installation on Linux",
"url": "https://labex.io/tutorials/linux-software-installation-on-linux-18005",
"type": "article"
}
]
},
"2oQiuQ2j02SCt9t5eV6hg": {
"title": "Package Repositories",
"description": "Package repositories are storage locations containing software packages for Linux distributions. They enable easy installation, updates, and dependency management through package managers like apt, yum, or dnf. Each distribution has pre-configured repositories with tested, secure packages. Use commands like `apt update` or `yum update` to sync with repositories.\n\n sudo apt update # command to update the repository in Ubuntu\n sudo yum update # command to update the repository in CentOS or Fedora\n raco pkg update # command in Racket to update all installed packages\n \n\nThese repositories are what make Linux a force to reckon with when it comes to software management with an element of security ensuring that the users only install software that is secure and reliable.",
"links": []
},
"Z23eJZjmWoeXQuezR9AhG": {
"title": "Finding & Installing Packages",
"description": "Linux package managers like `apt`, `yum`, and `dnf` automate software installation, updates, and removal. Use `apt-get update && apt-get install package-name` on Debian/Ubuntu systems or `dnf install package-name` on Fedora/CentOS. Package management eliminates manual compilation from source code and requires appropriate permissions (usually root access).\n\nFor example, on a Debian-based system like Ubuntu you would use `apt` or `apt-get` to install a new package like so:\n\n sudo apt-get update\n sudo apt-get install package-name\n \n\nWhile in a Fedora or CentOS you would use `dnf` or `yum`:\n\n sudo dnf update\n sudo dnf install package-name\n \n\nNote that you should replace `package-name` with the name of the package you want to install. Remember that you will need appropriate permissions (often root) to install packages in a Linux system.",
"links": []
},
"48wAoAAlCNt3j5mBpKTWC": {
"title": "Listing Installed Packages",
"description": "Linux distributions use different package managers: `apt` (Debian-based), `dnf` (Fedora), `zypper` (OpenSUSE), `pacman` (Arch). Listing installed packages helps with auditing software and deployment automation. Commands: `sudo apt list --installed` for apt systems, `dnf list installed` for dnf systems. Each distribution has its own syntax for this command.\n\nBelow is the command for listing installed packages in an `apt` package manager:\n\n sudo apt list --installed\n \n\nFor `dnf` package manager, you would use:\n\n dnf list installed\n \n\nRemember, different distributions will have their own syntax for this command.",
"links": []
},
"xEHiB-egkkcBuZmgMoqHT": {
"title": "Install / Remove / Upgrade Packages",
"description": "Package management in Linux involves installing, removing, and upgrading software using distribution-specific tools. Use `apt` for Debian/Ubuntu, `yum`/`dnf` for Fedora/RHEL/CentOS, and `zypper` for SUSE. Common operations include `install package-name`, `remove package-name`, and `upgrade` commands. Each package manager has specific syntax but similar functionality for software lifecycle management.\n\nA typical package management task such as installing a new package using `apt` would involve executing a command like:\n\n sudo apt-get install packagename\n \n\nHowever, the exact command varies depending on the package manager in use. Similarly, removing and upgrading packages also utilize command-line instructions specific to each package manager. Detailed understanding of these tasks is crucial for effective Linux system administration.",
"links": []
},
"eKyMZn30UxQeBZQ7FxFbF": {
"title": "Snap",
"description": "Snap is a modern Linux package management system by Canonical providing self-contained packages with all dependencies included. Snaps run consistently across different Linux distributions, install from Snapcraft store, and update automatically. Updates are transactional with automatic rollback on failure. Install packages using `sudo snap install [package-name]` command.\n\nHere is a simple example of a snap command:\n\n sudo snap install [package-name]",
"links": []
},
"Fn_uYKigJRgb7r_iYGVBr": {
"title": "Disks and Filesystems",
"description": "Linux supports various filesystems like EXT4, FAT32, NTFS, and Btrfs for organizing data on storage devices. Each filesystem has specific advantages - EXT4 for Linux systems, FAT32 for compatibility across operating systems. The `df -T` command displays mounted filesystems with their types, sizes, and available space information.\n\nHere's an example of how to display the type of filesystems of your mounted devices with the \"df\" command in Linux:\n\n df -T\n \n\nThe output shows the names of your disks, their filesystem types, and other additional information such as total space, used space, and available space on the disks.",
"links": []
},
"AwQJYL60NNbA5_z7iLcM7": {
"title": "Inodes",
"description": "An inode (index node) is a data structure in Linux filesystems that stores metadata about files and directories except their names and actual data. Contains file size, owner, permissions, timestamps, and more. Each file has a unique inode number for identification. Understanding inodes helps with advanced operations like linking and file recovery. Use `ls -i filename` to view inode numbers.\n\n # Retrieve the inode of a file\n ls -i filename\n \n\nLearn more from the following resources:",
"links": [
{
"title": "Introduction to Inodes",
"url": "https://linuxjourney.com/lesson/inodes",
"type": "article"
}
]
},
"LFPhSHOhUqM98fUxMjQUw": {
"title": "Filesystems",
"description": "Filesystems define how files are stored and organized on Linux storage disks, ensuring data integrity, reliability, and efficient access. Linux supports various types like EXT4, XFS, BTRFS with different performance and recovery capabilities. All files start from root directory '/'. Use `df -T` to display filesystem types and disk usage status. Essential for Linux administration tasks.\n\nA disk installed in a Linux system can be divided into multiple partitions, each with its own filesystem. Linux supports various types of filesystems, such as EXT4, XFS, BTRFS, etc. Each one of them has their own advantages regarding performance, data integrity and recovery options.\n\nConfiguration of these filesystems relies on a defined hierarchical structure. All the files and directories start from the root directory, presented by '/'.\n\nUnderstanding the concept and management of filesystems is key for the successful administration of Linux systems, as it involves routine tasks like mounting/unmounting drives, checking disk space, managing file permissions, and repairing corrupted filesystems.\n\nCode snippet to display the file system in Linux:\n\n df -T\n \n\nThis command will display the type of filesystem, along with the disk usage status.",
"links": []
},
"AWosNs2nvDGV8r6WvgBI1": {
"title": "Swap",
"description": "Swap space extends physical memory by using disk storage when RAM is full. Inactive memory pages move to swap, freeing RAM but with performance impact due to slower disk access. Swap can exist as dedicated partitions or regular files. Create with `fallocate`, `mkswap`, and `swapon` commands. Critical for memory management and system stability optimization.",
"links": [
{
"title": "Swap - Arch Wiki",
"url": "https://wiki.archlinux.org/title/Swap",
"type": "article"
},
{
"title": "zram (alternative) - Arch Wiki",
"url": "https://wiki.archlinux.org/title/Zram",
"type": "article"
}
]
},
"zmb5lK_EGMAChPoPvP9E0": {
"title": "Mounts",
"description": "Mounting in Linux attaches filesystems to specific directories (mount points) in the directory tree, allowing the OS to access data on storage devices. The `mount` command performs this operation. Example: `mount /dev/sdb1 /mnt` mounts second partition to `/mnt` directory. The `/mnt` directory is conventionally used for temporary mounting operations. Essential for Linux disk and filesystem management.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Mounting, unmounting and the /mnt directory - The Linux Documentation Project",
"url": "https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/mnt.html",
"type": "article"
},
{
"title": "Linux mount command with Examples",
"url": "https://phoenixnap.com/kb/linux-mount-command",
"type": "article"
},
{
"title": "The mount command manual page",
"url": "https://man7.org/linux/man-pages/man8/mount.8.html",
"type": "article"
}
]
},
"4xBaZPk0eSsWG1vK3e2yW": {
"title": "Adding Disks",
"description": "Adding disks in Linux involves partitioning, creating filesystems, and mounting. Use `lsblk` to list devices, `fdisk /dev/sdX` to create partitions, `mkfs.ext4 /dev/sdX1` to create filesystems, and `mount /dev/sdX1 /mount/point` to mount. This process prepares new storage devices for seamless integration into the Linux filesystem hierarchy.",
"links": []
},
"I3LNa1cM_zRkBy8wKdz3g": {
"title": "LVM",
"description": "LVM provides logical volume management through device mapper framework, offering flexible disk management with resizing, mirroring, and moving capabilities. Three levels: Physical Volumes (PVs - actual disks), Volume Groups (VGs - storage pools), and Logical Volumes (LVs - carved portions). Create with `pvcreate`, `vgcreate`, and `lvcreate` commands. Essential for enterprise storage systems.",
"links": []
},
"DQEa8LrJ9TVW4ULBE4aHJ": {
"title": "Booting Linux",
"description": "Linux booting involves several stages: POST, MBR, GRUB, Kernel, Init, and GUI/CLI. The bootloader loads the kernel into memory, which detects hardware, loads drivers, mounts filesystems, starts system processes, and presents login prompts. GRUB configuration is managed through `/etc/default/grub` with settings like timeout and default boot options.",
"links": []
},
"ru7mpLQZKE1QxAdiA1sS3": {
"title": "Logs",
"description": "Linux maintains logs documenting system activities, errors, and kernel messages. Boot logs record all operations during system startup for troubleshooting. Use `dmesg` to view kernel ring buffer messages in real-time, or access logs in `/var/log`. Systemd uses `journalctl` for logging. Log levels range from emergency (system unusable) to debug messages.",
"links": []
},
"o5lSQFW-V_PqndGqo1mp3": {
"title": "Boot Loaders",
"description": "Boot loaders load the OS kernel into memory when systems start. Common Linux boot loaders include GRUB (modern, feature-rich with graphical interface) and LILO (older, broader hardware support). Boot loaders initialize hardware, load drivers, start schedulers, and execute init processes. Use `sudo update-grub` to update GRUB configuration. Enable multi-OS booting on single machines.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "comprehensive documentation of Bootloader - archlinux wiki",
"url": "https://wiki.archlinux.org/title/Arch_boot_process#Boot_loader",
"type": "article"
},
{
"title": "What Is GRUB Bootloader in Linux?",
"url": "https://phoenixnap.com/kb/what-is-grub",
"type": "article"
},
{
"title": "The GNU GRUB website",
"url": "https://www.gnu.org/software/grub/",
"type": "article"
}
]
},
"Mb42VFjCzMZn_PovKIfKx": {
"title": "Networking",
"description": "Linux networking enables system connections and resource sharing across platforms with robust management tools. Network configurations stored in `/etc/network/interfaces`. Key commands include `ifconfig` (deprecated) and `ip` for interface management. Supports various protocols with excellent scalability. Essential for system connectivity and network troubleshooting.\n\nLinux adopts a file-based approach for network configuration, storing network-related settings and configurations in standard files, such as /etc/network/interfaces or /etc/sysconfig/network-scripts/, depending on the Linux distribution.\n\nPerhaps one of the most popular commands related to networking on a Linux system is the `ifconfig` command:\n\n ifconfig\n \n\nThis will output information about all network interfaces currently active on the system. However, please note that `ifconfig` is becoming obsolete and being replaced by `ip`, which offers more features and capabilities.",
"links": []
},
"0pciSsiQqIGJh3x8465_s": {
"title": "TCP/IP Stack",
"description": "TCP/IP (Transmission Control Protocol/Internet Protocol) is the foundational networking protocol suite that enables computer communication over networks. It consists of four layers: Network Interface, Internet, Transport, and Application. In Linux, TCP/IP is integral to the OS functionality, allowing hosts to connect and transfer data across same or different networks.\n\nBelow is a basic command using TCP/IP protocol in Linux:\n\n # To view all active TCP/IP network connections\n netstat -at",
"links": []
},
"Xszo9vXuwwXZo26seHehD": {
"title": "Subnetting",
"description": "Subnetting divides networks into smaller subnets to improve performance and security in Linux networking. It organizes IP addresses within IP addressing schemes, preventing conflicts and efficiently utilizing address ranges. Use `route -n` to view routing tables and `route add -net xxx.xxx.xxx.x/xx gw yyy.yyy.yyy.y` to add subnets. Essential for complex networking environments.",
"links": []
},
"4ees23q281J1DPVAc7iXd": {
"title": "Ethernet & arp/rarp",
"description": "Key networking protocols in Linux include Ethernet (LAN communication standard), ARP (Address Resolution Protocol - converts IP to MAC addresses), and RARP (Reverse ARP - converts MAC to IP addresses). These protocols enable local network communication and address resolution, essential for network troubleshooting and management in Linux systems.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "ARP Explained - Address Resolution Protocol",
"url": "https://www.youtube.com/watch?v=cn8Zxh9bPio",
"type": "video"
},
{
"title": "What is Ethernet?",
"url": "https://www.youtube.com/watch?v=HLziLmaYsO0",
"type": "video"
}
]
},
"X6Jw95kbyPgsDNRhvkQP9": {
"title": "DHCP",
"description": "DHCP (Dynamic Host Configuration Protocol) automatically allocates IP addresses and network configuration to clients, ensuring unique addresses for each machine. In Linux, install with `sudo apt-get install isc-dhcp-server` and configure via `/etc/dhcp/dhcpd.conf`. DHCP servers require static IPs for effective management and can handle DNS and network data.\n\nThe DHCP server effectively manages the IP addresses and information related to them, making sure that each client machine gets a unique IP and all the correct network information.\n\nIn Linux, DHCP can be configured and managed using terminal commands. This involves the installation of the DHCP server software, editing the configuration files, and managing the server's services.\n\nA traditional DHCP server should have a static IP address to manage the IP distribution effectively. The DHCP in Linux also handles DNS and other related data that your network might require.\n\nHere is an example of a basic command to install a DHCP server in a Debian-based Linux:\n\n sudo apt-get install isc-dhcp-server\n \n\nAfter the installation process, all configurations of the DHCP server are done in the configuration file located at `/etc/dhcp/dhcpd.conf` which can be edited using any text editor.",
"links": []
},
"D0yUzzaJsfhtdBWMtquAj": {
"title": "IP Routing",
"description": "IP routing in Linux involves configuring routing tables and network routes for packet forwarding across networks. The kernel handles route selection to send packets to their destinations. Use the `ip` command (replacing deprecated `ifconfig`) for network configuration. Example: `ip route show` displays all kernel-known routes for network troubleshooting and management.",
"links": []
},
"f5oQYhmjNM2_FD7Qe1zGK": {
"title": "DNS Resolution",
"description": "DNS (Domain Name System) converts hostnames to IP addresses, enabling users to access websites without remembering numeric addresses. Linux systems use `/etc/resolv.conf` to configure DNS resolution. Applications consult the DNS resolver, which communicates with DNS servers for address translation. Use `nslookup` or `dig` commands to query DNS and troubleshoot network connectivity issues.",
"links": []
},
"bZ8Yj6QfBeDdh8hRM_aZs": {
"title": "Netfilter",
"description": "Netfilter is a Linux kernel framework for manipulating and filtering network packets with hooks at various stages (prerouting, input, forward, output, postrouting). Used for firewalls and NAT management with iptables configuration. Essential for traffic control, packet modification, logging, and intrusion detection in Linux networking systems.",
"links": []
},
"uk6UMuI8Uhf02TBAGVeLS": {
"title": "SSH",
"description": "SSH (Secure Shell) is a cryptographic network protocol providing secure remote access, command execution, and data communication between networked computers. Replaces insecure protocols like Telnet with confidentiality, integrity, and security. Use `ssh username@server_ip_address` to connect to remote Linux servers. Essential for secure system administration and remote management.",
"links": []
},
"tVrbVcNEfc11FbEUoO2Dk": {
"title": "File Transfer",
"description": "Linux file transfer involves copying or moving files between systems over networks. Command-line tools support protocols like FTP, HTTP, SCP, SFTP, and NFS. Common commands include `scp`, `rsync`, and `wget`. Example: `scp /local/file username@remote:/destination` copies files to remote systems. These tools make network file sharing streamlined, easier, and more secure.",
"links": []
},
"4tFZ1PLpz50bddf7zSFrW": {
"title": "Shell Programming",
"description": "Shell programming (scripting) automates administrative tasks, repetitive operations, and system monitoring in Linux. Bash is the default shell and scripting language in most distributions. Scripts are text files executed by the shell, excellent for system automation. Example: `#!/bin/bash echo \"Hello, World!\"` creates a simple script that prints output to terminal.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Bash Scripting on Linux - YT Playlist",
"url": "https://youtube.com/playlist?list=PLT98CRl2KxKGj-VKtApD8-zCqSaN2mD4w&si=MSehStqnhSqgoMSj",
"type": "video"
}
]
},
"-pW7R76yNIeGf7TQoX4QL": {
"title": "Literals",
"description": "Shell literals are fixed values in source code including string literals (enclosed in quotes), numeric literals (sequences of digits), and boolean literals (1=true, 0=false). String examples: 'Hello, world!' or \"Hello, world!\". Numeric examples: 25, 100, 1234. Understanding literals is fundamental for shell scripting readability and functionality in Linux programming.",
"links": []
},
"JyxvZOb7iusOSUYSlniGl": {
"title": "Variables",
"description": "Shell variables store system or user-defined data that can change during script execution. Two categories exist: System Variables (PATH, HOME, PWD) created by Linux, and User-Defined Variables created by users. Define variables with `=` operator and retrieve values with `$` prefix. Example: `MY_VARIABLE=\"Hello World\"` then `echo $MY_VARIABLE` prints the value.",
"links": []
},
"WJT-yrMq8cEI87RHWA2jY": {
"title": "Loops",
"description": "Shell loops automate repetitive tasks with three types: `for` (iterates over lists), `while` (executes while condition true), `until` (runs until condition true). Example: `for i in 1 2 3; do echo \"$i\"; done` outputs each number. Essential for script efficiency, automation, and effective Linux shell programming.",
"links": []
},
"rQxfp7UWqN72iqewZhOdc": {
"title": "Conditionals",
"description": "Shell conditionals allow scripts to make decisions based on conditions using `if`, `elif`, and `else` statements. These control process flow by evaluating string variables, arithmetic tests, or process status. Conditions are checked sequentially - if true, the corresponding code block executes; otherwise, it moves to the next condition until finding a match or reaching `else`.",
"links": []
},
"rOGnHbGIr3xPCFdpkqoeK": {
"title": "Debugging",
"description": "Shell script debugging in Linux uses tools like bash's `-x` option for execution traces, `trap`, `set` commands, and external tools like `shellcheck`. Use `#!/bin/bash -x` in scripts or `bash -x script.sh` from command line for tracing. These debugging options help detect, trace, and fix errors to make scripts more efficient and error-proof.\n\nVisit the following sources to learn more:",
"links": [
{
"title": "Official Bashdb Documentation",
"url": "https://bashdb.readthedocs.io/en/latest/",
"type": "article"
}
]
},
"bdQNcr1sj94aX_gjwf2Fa": {
"title": "Troubleshooting",
"description": "Linux troubleshooting involves identifying and resolving system errors, hardware/software issues, network problems, and resource management challenges. Key skills include using command-line tools, inspecting log files, understanding processes, and interpreting error messages. Tools like `top` provide real-time process monitoring to identify resource-heavy processes causing performance issues efficiently.",
"links": []
},
"Ymf3u_sG1dyt8ZR_LbwqJ": {
"title": "ICMP",
"description": "Internet Control Message Protocol (ICMP) is a supportive protocol used by network devices to communicate error messages and operational information. Essential for Linux network troubleshooting, ICMP enables tools like `ping` and `traceroute` to diagnose network connectivity and routing issues. Use `ping www.google.com` to send ICMP echo requests and test network reachability effectively.",
"links": []
},
"Uc36t92UAlILgM3_XxcMG": {
"title": "ping",
"description": "The `ping` command is essential for Linux network troubleshooting, checking connectivity between your host and target machines. It sends ICMP ECHO\\_REQUEST packets and listens for ECHO\\_RESPONSE returns, providing insights into connection health and speed. Use `ping <target IP or hostname>` to diagnose network connectivity issues and identify reachability problems efficiently.",
"links": []
},
"BnB3Rirh4R7a7LW7-k-95": {
"title": "traceroute",
"description": "Traceroute is a Linux network diagnostic tool that displays the path packets take from your system to a destination. It identifies routing problems, measures latency, and reveals network structure as packets traverse the internet. Each hop is tested multiple times with round-trip times displayed. Use `traceroute www.example.com` to discover packet routes and diagnose failures.",
"links": []
},
"yrxNYMluJ9OAQCKuM5W1u": {
"title": "netstat",
"description": "Netstat is a command-line tool for network troubleshooting and performance measurement in Linux. It provides network statistics, open ports, routing table information, and protocol details. Use options like `-n` for numerical addresses, `-c` for continuous monitoring, and `-t`/`-u` for specific protocols. Example: `netstat -n` lists all connections with numerical values.",
"links": []
},
"7seneb4TWts4v1_x8xlcZ": {
"title": "Packet Analysis",
"description": "Packet analysis is a key Linux network troubleshooting skill involving capturing and analyzing network traffic to identify performance issues, connectivity problems, and security vulnerabilities. Tools like tcpdump and Wireshark provide packet-level details for network diagnostics. Use `sudo tcpdump -i eth0` to capture packets on the eth0 interface for debugging network protocols.",
"links": []
},
"3OpGaQhyNtk1n1MLp-tlb": {
"title": "Containerization",
"description": "Containerization is a virtualization method that encapsulates applications in containers with isolated operating environments, enabling reliable deployment across computing environments. Unlike VMs requiring full operating systems, containers share the host system's user space, making them lightweight and faster. Docker is a popular Linux containerization tool for managing complex applications.",
"links": []
},
"QgfenmhMc18cU_JngQ1n0": {
"title": "ulimits",
"description": "Ulimits (user limits) are Linux kernel features that restrict resources like file handles and memory that processes can consume. In containerization, ulimits prevent rogue processes from exhausting server resources and creating denial-of-service situations. Use `ulimit -a` to view current limits and `ulimit -n 1024` to set specific limits for optimal container performance and security.\n\n # To see current ulimits:\n ulimit -a\n \n # To set a specific ulimit (soft limit), for example file handles:\n ulimit -n 1024\n \n\nProperly configuring and understanding ulimits especially in containerized environments is an essential part of system administration in Linux.",
"links": []
},
"23lsrUw8ux6ZP9JlDNNu2": {
"title": "cgroups",
"description": "Cgroups (control groups) are a Linux kernel feature that organizes processes into hierarchical groups and limits their resource usage (CPU, memory, disk I/O). Essential for containerization, cgroups prevent containers from monopolizing host resources, ensuring system stability and performance. Use `cgcreate` to create groups, assign processes, and set resource limits effectively.\n\nHere's an example of how you might create a new cgroup for a container:\n\n # Create a new cgroup for a container;\n sudo cgcreate -g cpu:/my_new_container\n \n # Assign the current shell's process to the new cgroup;\n echo $$ | sudo tee /sys/fs/cgroup/cpu/my_new_container/tasks\n \n # Limit the CPU usage of the cgroup to 20%;\n echo 200000 | sudo tee /sys/fs/cgroup/cpu/my_new_container/cpu.cfs_quota_us\n \n\nIn this snippet, we are using `cgcreate` to create a new cgroup, then adding the current process to it, and finally setting a CPU limit.",
"links": []
},
"bVCwRoFsYb3HD8X4xuKOo": {
"title": "Container Runtime",
"description": "Container runtime is software responsible for running containers in Linux, providing image transport, storage, execution, and network interactions. Popular options include Docker (comprehensive ecosystem), Containerd (lightweight standalone), and CRI-O (Kubernetes-optimized). Each runtime offers specific features and benefits for different use cases in containerized application deployment and management.",
"links": []
},
"MfengY3ouz6sSOx3PXYf8": {
"title": "Docker",
"description": "Docker is an open-source containerization platform that uses OS-level virtualization to package applications with dependencies into lightweight containers. In Linux, Docker containers share the kernel and use features like namespaces and cgroups for isolation. This provides less overhead than traditional VMs while enabling consistent deployment across environments.\n\nHere's a basic example of running an application (for example, hello-world) with Docker on Linux:\n\n # Pull the Docker image from Docker Hub\n sudo docker pull hello-world\n \n # Run the Docker container\n sudo docker run hello-world\n \n\nThe above commands allow you to download a Docker image and run it on your Linux system, providing the foundation for deploying containers in development, testing, and production environments.",
"links": []
}
}

View File

@@ -1,44 +1,8 @@
{
"GISOFMKvnBys0O0IMpz2J": {
"title": "Learn the Basics",
"description": "Python is a high-level, interpreted, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically-typed and garbage-collected.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Python Website",
"url": "https://www.python.org/",
"type": "article"
},
{
"title": "Python - Wiki",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"type": "article"
},
{
"title": "Tutorial Series: How to Code in Python",
"url": "https://www.digitalocean.com/community/tutorials/how-to-write-your-first-python-3-program",
"type": "article"
},
{
"title": "Google's Python Class",
"url": "https://developers.google.com/edu/python",
"type": "article"
},
{
"title": "W3Schools - Python Tutorial",
"url": "https://www.w3schools.com/python",
"type": "article"
},
{
"title": "Explore top posts about Python",
"url": "https://app.daily.dev/tags/python?ref=roadmapsh",
"type": "article"
},
{
"title": "Learn Python - Full Course",
"url": "https://www.youtube.com/watch?v=4M87qBgpafk",
"type": "video"
}
]
"description": "",
"links": []
},
"6xRncUs3_vxVbDur567QA": {
"title": "Basic Syntax",
@@ -297,6 +261,119 @@
]
},
"OPD4WdMO7q4gRZMcRCQh1": {
"title": "Arrays and Linked Lists",
"description": "",
"links": []
},
"0NlRczh6ZEaFLlT6LORWz": {
"title": "Heaps, Stacks and Queues",
"description": "",
"links": []
},
"DG4fi1e5ec2BVckPLsTJS": {
"title": "Hash Tables",
"description": "",
"links": []
},
"uJIqgsqUbE62Tyo3K75Qx": {
"title": "Binary Search Tree",
"description": "",
"links": []
},
"kLBgy_nxxjE8SxdVi04bq": {
"title": "Recursion",
"description": "",
"links": []
},
"vvTmjcWCVclOPY4f_5uB0": {
"title": "Sorting Algorithms",
"description": "",
"links": []
},
"274uk28wzxn6EKWQzLpHs": {
"title": "Modules",
"description": "Modules refer to a file containing Python statements and definitions. A file containing Python code, for example: `example.py`, is called a module, and its module name would be example. We use modules to break down large programs into small manageable and organized files. Furthermore, modules provide reusability of code.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Python Modules",
"url": "https://docs.python.org/3/tutorial/modules.html",
"type": "article"
},
{
"title": "Modules in Python",
"url": "https://www.programiz.com/python-programming/modules",
"type": "article"
}
]
},
"JDDG4KfhtIlw1rkNCzUli": {
"title": "Learn the Basics",
"description": "Python is a high-level, interpreted, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically-typed and garbage-collected.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Python Website",
"url": "https://www.python.org/",
"type": "article"
},
{
"title": "Python - Wiki",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"type": "article"
},
{
"title": "Tutorial Series: How to Code in Python",
"url": "https://www.digitalocean.com/community/tutorials/how-to-write-your-first-python-3-program",
"type": "article"
},
{
"title": "Google's Python Class",
"url": "https://developers.google.com/edu/python",
"type": "article"
},
{
"title": "W3Schools - Python Tutorial",
"url": "https://www.w3schools.com/python",
"type": "article"
},
{
"title": "Explore top posts about Python",
"url": "https://app.daily.dev/tags/python?ref=roadmapsh",
"type": "article"
},
{
"title": "Learn Python - Full Course",
"url": "https://www.youtube.com/watch?v=4M87qBgpafk",
"type": "video"
}
]
},
"VJSIbYJcy2MC6MOFBrqXi": {
"title": "Data Structures & Algorithms",
"description": "A data structure is a named location that can be used to store and organize data. And, an algorithm is a collection of steps to solve a particular problem. Learning data structures and algorithms allow us to write efficient and optimized computer programs.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Visit Dedicated DSA Roadmap",
"url": "https://roadmap.sh/datastructures-and-algorithms",
"type": "article"
},
{
"title": "Learn DS & Algorithms",
"url": "https://www.programiz.com/dsa",
"type": "article"
},
{
"title": "Explore top posts about Algorithms",
"url": "https://app.daily.dev/tags/algorithms?ref=roadmapsh",
"type": "article"
},
{
"title": "Data Structures Illustrated",
"url": "https://www.youtube.com/playlist?list=PLkZYeFmDuaN2-KUIv-mvbjfKszIGJ4FaY",
"type": "video"
}
]
},
"kIuns7FOwapwtFLKo1phQ": {
"title": "Arrays and Linked Lists",
"description": "Arrays store elements in contiguous memory locations, resulting in easily calculable addresses for the elements stored and this allows faster access to an element at a specific index. Linked lists are less rigid in their storage structure and elements are usually not stored in contiguous locations, hence they need to be stored with additional tags giving a reference to the next element. This difference in the data storage scheme decides which data structure would be more suitable for a given situation.\n\nVisit the following resources to learn more:",
"links": [
@@ -322,7 +399,7 @@
}
]
},
"0NlRczh6ZEaFLlT6LORWz": {
"rSfg5M65LyZldhrdWOr90": {
"title": "Heaps, Stacks and Queues",
"description": "**Stacks:** Operations are performed LIFO (last in, first out), which means that the last element added will be the first one removed. A stack can be implemented using an array or a linked list. If the stack runs out of memory, its called a stack overflow.\n\n**Queue:** Operations are performed FIFO (first in, first out), which means that the first element added will be the first one removed. A queue can be implemented using an array.\n\n**Heap:** A tree-based data structure in which the value of a parent node is ordered in a certain way with respect to the value of its child node(s). A heap can be either a min heap (the value of a parent node is less than or equal to the value of its children) or a max heap (the value of a parent node is greater than or equal to the value of its children).\n\nVisit the following resources to learn more:",
"links": [
@@ -358,7 +435,7 @@
}
]
},
"DG4fi1e5ec2BVckPLsTJS": {
"0-m8jVuDKE8hX1QorKGTM": {
"title": "Hash Tables",
"description": "Hash Table, Map, HashMap, Dictionary or Associative are all the names of the same data structure. It is a data structure that implements a set abstract data type, a structure that can map keys to values.\n\nVisit the following resources to learn more:",
"links": [
@@ -379,7 +456,7 @@
}
]
},
"uJIqgsqUbE62Tyo3K75Qx": {
"7NZlydjm4432vLY1InBS7": {
"title": "Binary Search Tree",
"description": "A binary search tree, also called an ordered or sorted binary tree, is a rooted binary tree data structure with the key of each internal node being greater than all the keys in the respective node's left subtree and less than the ones in its right subtree\n\nVisit the following resources to learn more:",
"links": [
@@ -400,7 +477,7 @@
}
]
},
"kLBgy_nxxjE8SxdVi04bq": {
"94KnPMQdNTOwQkUv37tAk": {
"title": "Recursion",
"description": "Recursion is a method of solving a computational problem where the solution depends on solutions to smaller instances of the same problem. Recursion solves such recursive problems by using functions that call themselves from within their own code.\n\nVisit the following resources to learn more:",
"links": [
@@ -416,7 +493,7 @@
}
]
},
"vvTmjcWCVclOPY4f_5uB0": {
"YNptpfK9qv2ovmkUXLkJT": {
"title": "Sorting Algorithms",
"description": "Sorting refers to arranging data in a particular format. Sorting algorithm specifies the way to arrange data in a particular order. Most common orders are in numerical or lexicographical order. The importance of sorting lies in the fact that data searching can be optimized to a very high level, if data is stored in a sorted manner.\n\nVisit the following resources to learn more:",
"links": [
@@ -437,48 +514,6 @@
}
]
},
"274uk28wzxn6EKWQzLpHs": {
"title": "Modules",
"description": "Modules refer to a file containing Python statements and definitions. A file containing Python code, for example: `example.py`, is called a module, and its module name would be example. We use modules to break down large programs into small manageable and organized files. Furthermore, modules provide reusability of code.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Python Modules",
"url": "https://docs.python.org/3/tutorial/modules.html",
"type": "article"
},
{
"title": "Modules in Python",
"url": "https://www.programiz.com/python-programming/modules",
"type": "article"
}
]
},
"VJSIbYJcy2MC6MOFBrqXi": {
"title": "Data Structures & Algorithms",
"description": "A data structure is a named location that can be used to store and organize data. And, an algorithm is a collection of steps to solve a particular problem. Learning data structures and algorithms allow us to write efficient and optimized computer programs.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Visit Dedicated DSA Roadmap",
"url": "https://roadmap.sh/datastructures-and-algorithms",
"type": "article"
},
{
"title": "Learn DS & Algorithms",
"url": "https://www.programiz.com/dsa",
"type": "article"
},
{
"title": "Explore top posts about Algorithms",
"url": "https://app.daily.dev/tags/algorithms?ref=roadmapsh",
"type": "article"
},
{
"title": "Data Structures Illustrated",
"url": "https://www.youtube.com/playlist?list=PLkZYeFmDuaN2-KUIv-mvbjfKszIGJ4FaY",
"type": "video"
}
]
},
"08XifLQ20c4FKI_4AWNBQ": {
"title": "Builtin",
"description": "Python has a rich standard library of built-in modules that provide a wide range of functionality. Some of the most commonly used built-in modules include: sys, os, math, datetime, random, re, itertools, etc.\n\nVisit the following resources to learn more:",
@@ -771,11 +806,6 @@
"title": "Poetry Docs",
"url": "https://python-poetry.org/docs/",
"type": "article"
},
{
"title": "Python Poetry - Basics",
"url": "https://www.youtube.com/watch?v=Ji2XDxmXSOM",
"type": "video"
}
]
},

View File

@@ -2150,11 +2150,6 @@
"title": "Git & GitHub Crash Course For Beginners",
"url": "https://www.youtube.com/watch?v=SWYqp7iY_Tc",
"type": "video"
},
{
"title": "Complete Git and GitHub Tutorial",
"url": "https://www.youtube.com/watch?v=apGV9Kg7ics",
"type": "video"
}
]
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -564,7 +564,7 @@
"type": "article"
},
{
"title": "SQL Constraints",
"title": "SQL Contraints",
"url": "https://www.programiz.com/sql/constraints",
"type": "article"
}

View File

@@ -196,7 +196,7 @@
},
"_C-55tciBzc6_Kyk6272k": {
"title": "Replace the Routine",
"description": "In the UX design process, understanding and working with existing user behavior is crucial. One key aspect of this is the concept of \"replace the routine\". This involves observing and analyzing the current habits and routines of your users, and then designing your product around it.\n\nReplacing the routine in UX design is about finding more efficient, delightful, and engaging ways for users to complete their tasks. You should not look to force a completely new set of behaviors upon your users but instead improve their experience by offering a better alternative to their existing habits.\n\nConsider the following points when replacing the routine:\n\n* **Understand the user's context**: Study the users life cycle and create personas to better comprehend their needs. This helps you identify their preferences, pain points, and habits, which in turn enables the creation of a meaningful and effective design.\n \n* **Identify the existing routine**: Analyze the current habits and routines of your users. What are the steps they are used to taking in order to complete the task? This information will be vital in designing a product that smoothly replaces their existing routine with an improved one.\n \n* **Design an improved routine**: Create a new user flow that achieves the same goal but in a manner that is more efficient, simpler, and more intuitive for the user. This new routine should leverage the knowledge you have gained about your users and their preferences.\n \n* **Test the new routine**: The importance of usability testing cannot be overstated. Validate your design by having real users test it out, and gather feedback to identify any areas that can be further optimized. Ensure that the new routine actually improves upon the existing one and doesn't create any new confusion.\n \n* **Iterate and refine**: UX design is an ongoing process. Continuously refine and optimize the new routine based on the user feedback and changing user behavior trends.\n \n\nBy adopting the \"replace the routine\" approach in your UX design, you can provide your users with a better experience that aligns with their existing behaviors, while also introducing new efficiencies and possibilities. Doing so increases user satisfaction, promotes adoption, and ultimately leads to happier, loyal users.",
"description": "In the UX design process, understanding and working with existing user behavior is crucial. One key aspect of this is the concept of \"replace the routine\". This involves observing and analyzing the current habits and routines of your users, and then designing your product around it.\n\nReplacing the routine in UX design is about finding more efficient, delightful, and engaging ways for users to complete their tasks. You should not look to force a completely new set of behaviors upon your users but instead improve their experience by offering a better alternative to their existing habits.\n\nConsider the following points when replacing the routine:\n\n* **Understand the user's context**: Study the users life cycle and create personas to better comprehend their . This helps you identify their preferences, pain points, and habits, which in turn enables the creation of a meaningful and effective design.\n \n* **Identify the existing routine**: Analyze the current habits and routines of your users. What are the steps they are used to taking in order to complete the task? This information will be vital in designing a product that smoothly replaces their existing routine with an improved one.\n \n* **Design an improved routine**: Create a new user flow that achieves the same goal but in a manner that is more efficient, simpler, and more intuitive for the user. This new routine should leverage the knowledge you have gained about your users and their preferences.\n \n* **Test the new routine**: The importance of usability testing cannot be overstated. Validate your design by having real users test it out, and gather feedback to identify any areas that can be further optimized. Ensure that the new routine actually improves upon the existing one and doesn't create any new confusion.\n \n* **Iterate and refine**: UX design is an ongoing process. Continuously refine and optimize the new routine based on the user feedback and changing user behavior trends.\n \n\nBy adopting the \"replace the routine\" approach in your UX design, you can provide your users with a better experience that aligns with their existing behaviors, while also introducing new efficiencies and possibilities. Doing so increases user satisfaction, promotes adoption, and ultimately leads to happier, loyal users.",
"links": []
},
"use-consciousness-to-interfere@0MbrHG-VDrdZqQ0jWtiDo.md": {
@@ -398,19 +398,8 @@
},
"t46s6Piyd8MoJYzdDTsjr": {
"title": "Figma",
"description": "[Figma](https://www.figma.com/) is a powerful and versatile web-based design tool that allows designers, developers, and stakeholders to collaborate on UI and UX projects in real-time. It's an excellent choice for creating wireframes and high-fidelity prototypes and supports vector editing, responsive design, and team collaboration.\n\nKey Features\n------------\n\n* **Real-time collaboration**: Figma lets multiple users work on a single design simultaneously, so teams can easily collaborate and see each other's changes in real-time.\n \n* **Platform-independent**: As a web-based tool, Figma is accessible from any device with a browser and an internet connection. It is compatible with Windows, macOS, and Linux.\n \n* **Components**: Figma uses a 'component' system, which allows you to reuse elements across your designs. By creating a master component, any updates made to the master will be reflected in all instances, helping to keep your designs consistent.\n \n* **Prototyping**: Figma allows you to create interactive prototypes of your designs using built-in prototyping features, including animations and transitions. This helps you to communicate the intended user experience to stakeholders and to test your designs with users.\n \n* **Version history**: Figma automatically saves your work and maintains a version history that lets you go back to any previous version of your design.\n \n* **Plugins**: Figma supports a wide range of user-created plugins that extend its functionality, allowing you to tailor the tool to your specific needs.\n \n\nVisit the following resources to learn more:",
"links": [
{
"title": "Figma Website",
"url": "https://figma.com",
"type": "article"
},
{
"title": "Free Figma UX Design UI Essentials Course",
"url": "https://youtu.be/kbZejnPXyLM?si=W9oY5j_6AcSuHfq2",
"type": "video"
}
]
"description": "[Figma](https://www.figma.com/) is a powerful and versatile web-based design tool that allows designers, developers, and stakeholders to collaborate on UI and UX projects in real-time. It's an excellent choice for creating wireframes and high-fidelity prototypes and supports vector editing, responsive design, and team collaboration.\n\nKey Features\n------------",
"links": []
},
"HI_urBhPqT0m3AeBQJIej": {
"title": "Adobe XD",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

View File

@@ -241,19 +241,19 @@ async function generateGuideOpenGraph() {
const image =
author?.imageUrl || 'https://roadmap.sh/images/default-avatar.png';
const isExternalImage = image?.startsWith('http');
let authorImageExtension = '';
let authorImageExtention = '';
let authorAvatar;
if (!isExternalImage) {
authorAvatar = await fs.readFile(path.join(ALL_AUTHOR_IMAGE_DIR, image));
authorImageExtension = image?.split('.')[1];
authorImageExtention = image?.split('.')[1];
}
let template = getGuideTemplate({
const template = getGuideTemplate({
...guide,
authorName: author.name,
authorAvatar: isExternalImage
? image
: `data:image/${authorImageExtension};base64,${authorAvatar.toString('base64')}`,
: `data:image/${authorImageExtention};base64,${authorAvatar.toString('base64')}`,
});
if (
hasSpecialCharacters(guide.title) ||

View File

@@ -1,131 +0,0 @@
.ai-chat .prose ul li > code,
.ai-chat .prose ol li > code,
.ai-chat p code,
.ai-chat a > code,
.ai-chat strong > code,
.ai-chat em > code,
.ai-chat h1 > code,
.ai-chat h2 > code,
.ai-chat h3 > code {
background: #ebebeb !important;
color: currentColor !important;
font-size: 14px;
font-weight: normal !important;
}
.ai-chat .course-ai-content.course-content.prose ul li > code,
.ai-chat .course-ai-content.course-content.prose ol li > code,
.ai-chat .course-ai-content.course-content.prose p code,
.ai-chat .course-ai-content.course-content.prose a > code,
.ai-chat .course-ai-content.course-content.prose strong > code,
.ai-chat .course-ai-content.course-content.prose em > code,
.ai-chat .course-ai-content.course-content.prose h1 > code,
.ai-chat .course-ai-content.course-content.prose h2 > code,
.ai-chat .course-ai-content.course-content.prose h3 > code,
.ai-chat .course-notes-content.prose ul li > code,
.ai-chat .course-notes-content.prose ol li > code,
.ai-chat .course-notes-content.prose p code,
.ai-chat .course-notes-content.prose a > code,
.ai-chat .course-notes-content.prose strong > code,
.ai-chat .course-notes-content.prose em > code,
.ai-chat .course-notes-content.prose h1 > code,
.ai-chat .course-notes-content.prose h2 > code,
.ai-chat .course-notes-content.prose h3 > code {
font-size: 12px !important;
}
.ai-chat .course-ai-content pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
.ai-chat .course-ai-content pre::-webkit-scrollbar {
display: none;
}
.ai-chat .course-ai-content pre,
.ai-chat .course-notes-content pre {
overflow: scroll;
font-size: 15px;
margin: 10px 0;
}
.ai-chat .prose ul li > code:before,
.ai-chat p > code:before,
.ai-chat .prose ul li > code:after,
.prose ol li > code:before,
p > code:before,
.ai-chat .prose ol li > code:after,
.ai-chat .course-content h1 > code:after,
.ai-chat .course-content h1 > code:before,
.ai-chat .course-content h2 > code:after,
.ai-chat .course-content h2 > code:before,
.ai-chat .course-content h3 > code:after,
.ai-chat .course-content h3 > code:before,
.ai-chat .course-content h4 > code:after,
.ai-chat .course-content h4 > code:before,
.ai-chat p > code:after,
.ai-chat a > code:after,
.ai-chat a > code:before {
content: '' !important;
}
.ai-chat .course-content.prose ul li > code,
.ai-chat .course-content.prose ol li > code,
.ai-chat .course-content p code,
.ai-chat .course-content a > code,
.ai-chat .course-content strong > code,
.ai-chat .course-content em > code,
.ai-chat .course-content h1 > code,
.ai-chat .course-content h2 > code,
.ai-chat .course-content h3 > code,
.ai-chat .course-content table code {
background: #f4f4f5 !important;
border: 1px solid #282a36 !important;
color: #282a36 !important;
padding: 2px 4px;
border-radius: 5px;
font-size: 16px !important;
white-space: pre;
font-weight: normal;
}
.ai-chat .course-content blockquote {
font-style: normal;
}
.ai-chat .course-content.prose blockquote h1,
.ai-chat .course-content.prose blockquote h2,
.ai-chat .course-content.prose blockquote h3,
.ai-chat .course-content.prose blockquote h4 {
font-style: normal;
margin-bottom: 8px;
}
.ai-chat .course-content.prose ul li > code:before,
.ai-chat .course-content p > code:before,
.ai-chat .course-content.prose ul li > code:after,
.ai-chat .course-content p > code:after,
.ai-chat .course-content h2 > code:after,
.ai-chat .course-content h2 > code:before,
.ai-chat .course-content table code:before,
.ai-chat .course-content table code:after,
.ai-chat .course-content a > code:after,
.ai-chat .course-content a > code:before,
.ai-chat .course-content h2 code:after,
.ai-chat .course-content h2 code:before,
.ai-chat .course-content h2 code:after,
.ai-chat .course-content h2 code:before {
content: '' !important;
}
.ai-chat .course-content table {
border-collapse: collapse;
border: 1px solid black;
border-radius: 5px;
}
.ai-chat .course-content table td,
.ai-chat .course-content table th {
padding: 5px 10px;
}

View File

@@ -1,562 +0,0 @@
import './AIChat.css';
import {
ArrowDownIcon,
FileUpIcon,
LockIcon,
PersonStandingIcon,
SendIcon,
TrashIcon,
} from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import AutogrowTextarea from 'react-textarea-autosize';
import { QuickHelpPrompts } from './QuickHelpPrompts';
import { QuickActionButton } from './QuickActionButton';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { useMutation, useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { useToast } from '../../hooks/use-toast';
import { markdownToHtml } from '../../lib/markdown';
import { ChatHistory } from './ChatHistory';
import { PersonalizedResponseForm } from './PersonalizedResponseForm';
import { userPersonaOptions } from '../../queries/user-persona';
import { UploadResumeModal } from './UploadResumeModal';
import { userResumeOptions } from '../../queries/user-resume';
import { httpPost } from '../../lib/query-http';
import {
renderMessage,
type MessagePartRenderer,
} from '../../lib/render-chat-message';
import { RoadmapRecommendations } from '../RoadmapAIChat/RoadmapRecommendations';
import { AIChatCourse } from './AIChatCouse';
import { showLoginPopup } from '../../lib/popup';
import { readChatStream } from '../../lib/chat';
import { chatHistoryOptions } from '../../queries/chat-history';
import { cn } from '../../lib/classname';
import type { RoadmapAIChatHistoryType } from '../../hooks/use-roadmap-ai-chat';
export const aiChatRenderer: Record<string, MessagePartRenderer> = {
'roadmap-recommendations': (options) => {
return <RoadmapRecommendations {...options} />;
},
'generate-course': (options) => {
return <AIChatCourse {...options} />;
},
};
type AIChatProps = {
messages?: RoadmapAIChatHistoryType[];
chatHistoryId?: string;
setChatHistoryId?: (chatHistoryId: string) => void;
onUpgrade?: () => void;
};
export function AIChat(props: AIChatProps) {
const {
messages: defaultMessages,
chatHistoryId: defaultChatHistoryId,
setChatHistoryId: setDefaultChatHistoryId,
onUpgrade,
} = props;
const toast = useToast();
const [message, setMessage] = useState('');
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const [streamedMessage, setStreamedMessage] =
useState<React.ReactNode | null>(null);
const [aiChatHistory, setAiChatHistory] = useState<
RoadmapAIChatHistoryType[]
>(defaultMessages ?? []);
const [isPersonalizedResponseFormOpen, setIsPersonalizedResponseFormOpen] =
useState(false);
const [isUploadResumeModalOpen, setIsUploadResumeModalOpen] = useState(false);
const [showScrollToBottomButton, setShowScrollToBottomButton] =
useState(false);
const scrollableContainerRef = useRef<HTMLDivElement>(null);
const chatContainerRef = useRef<HTMLDivElement>(null);
const textareaMessageRef = useRef<HTMLTextAreaElement>(null);
const { data: tokenUsage, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const { data: userPersona, isLoading: isUserPersonaLoading } = useQuery(
userPersonaOptions(),
queryClient,
);
const { data: userResume, isLoading: isUserResumeLoading } = useQuery(
userResumeOptions(),
queryClient,
);
const { mutate: deleteChatMessage, isPending: isDeletingChatMessage } =
useMutation(
{
mutationFn: (messages: RoadmapAIChatHistoryType[]) => {
if (!defaultChatHistoryId) {
return Promise.resolve({
status: 200,
message: 'Chat history not found',
});
}
return httpPost(`/v1-delete-chat-message/${defaultChatHistoryId}`, {
messages,
});
},
onSuccess: () => {
textareaMessageRef.current?.focus();
queryClient.invalidateQueries(
chatHistoryOptions(defaultChatHistoryId),
);
},
onError: (error) => {
toast.error(error?.message || 'Failed to delete message');
},
},
queryClient,
);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
const handleChatSubmit = () => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isLimitExceeded) {
if (!isPaidUser) {
onUpgrade?.();
}
toast.error('Limit reached for today. Please wait until tomorrow.');
return;
}
const trimmedMessage = message.trim();
if (!trimmedMessage || isStreamingMessage) {
return;
}
const newMessages: RoadmapAIChatHistoryType[] = [
...aiChatHistory,
{
role: 'user',
content: trimmedMessage,
// it's just a simple message, so we can use markdownToHtml
html: markdownToHtml(trimmedMessage),
},
];
flushSync(() => {
setAiChatHistory(newMessages);
setMessage('');
});
setTimeout(() => {
scrollToBottom();
}, 0);
textareaMessageRef.current?.focus();
completeAIChat(newMessages);
};
const canScrollToBottom = useCallback(() => {
const scrollableContainer = scrollableContainerRef?.current;
if (!scrollableContainer) {
return false;
}
const paddingBottom = parseInt(
getComputedStyle(scrollableContainer).paddingBottom,
);
const distanceFromBottom =
scrollableContainer.scrollHeight -
(scrollableContainer.scrollTop + scrollableContainer.clientHeight) -
paddingBottom;
return distanceFromBottom > -(paddingBottom - 80);
}, []);
const scrollToBottom = useCallback(
(behavior: 'instant' | 'smooth' = 'smooth') => {
const scrollableContainer = scrollableContainerRef?.current;
if (!scrollableContainer) {
return;
}
scrollableContainer.scrollTo({
top: scrollableContainer.scrollHeight,
behavior: behavior === 'instant' ? 'instant' : 'smooth',
});
},
[scrollableContainerRef],
);
const completeAIChat = async (
messages: RoadmapAIChatHistoryType[],
force: boolean = false,
) => {
setIsStreamingMessage(true);
const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/v1-chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
chatHistoryId: defaultChatHistoryId,
messages,
force,
}),
});
if (!response.ok) {
const data = await response.json();
toast.error(data?.message || 'Something went wrong');
setAiChatHistory([...messages].slice(0, messages.length - 1));
setIsStreamingMessage(false);
if (data.status === 401) {
removeAuthToken();
window.location.reload();
}
}
const stream = response.body;
if (!stream) {
setIsStreamingMessage(false);
toast.error('Something went wrong');
return;
}
await readChatStream(stream, {
onMessage: async (content) => {
const jsx = await renderMessage(content, aiChatRenderer, {
isLoading: true,
});
flushSync(() => {
setStreamedMessage(jsx);
});
setShowScrollToBottomButton(canScrollToBottom());
},
onMessageEnd: async (content) => {
const jsx = await renderMessage(content, aiChatRenderer, {
isLoading: false,
});
const newMessages: RoadmapAIChatHistoryType[] = [
...messages,
{
role: 'assistant',
content,
jsx,
},
];
flushSync(() => {
setStreamedMessage(null);
setIsStreamingMessage(false);
setAiChatHistory(newMessages);
});
queryClient.invalidateQueries(getAiCourseLimitOptions());
queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'list-chat-history';
},
});
},
onDetails: (details) => {
const chatHistoryId = details?.chatHistoryId;
if (!chatHistoryId) {
return;
}
setDefaultChatHistoryId?.(chatHistoryId);
},
});
setIsStreamingMessage(false);
};
const { mutate: uploadResume, isPending: isUploading } = useMutation(
{
mutationFn: (formData: FormData) => {
return httpPost('/v1-upload-resume', formData);
},
onSuccess: () => {
toast.success('Resume uploaded successfully');
setIsUploadResumeModalOpen(false);
queryClient.invalidateQueries(userResumeOptions());
},
onError: (error) => {
toast.error(error?.message || 'Failed to upload resume');
},
onMutate: () => {
setIsUploadResumeModalOpen(false);
},
},
queryClient,
);
useEffect(() => {
const scrollableContainer = scrollableContainerRef.current;
if (!scrollableContainer) {
return;
}
const abortController = new AbortController();
let timeoutId: NodeJS.Timeout;
const debouncedHandleScroll = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
setShowScrollToBottomButton(canScrollToBottom());
}, 100);
};
debouncedHandleScroll();
scrollableContainer.addEventListener('scroll', debouncedHandleScroll, {
signal: abortController.signal,
});
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
abortController.abort();
};
}, [aiChatHistory]);
const handleRegenerate = useCallback(
(index: number) => {
if (isLimitExceeded) {
if (!isPaidUser) {
onUpgrade?.();
}
toast.error('Limit reached for today. Please wait until tomorrow.');
return;
}
const filteredChatHistory = aiChatHistory.slice(0, index);
flushSync(() => {
setAiChatHistory(filteredChatHistory);
});
scrollToBottom();
completeAIChat(filteredChatHistory, true);
},
[aiChatHistory],
);
const handleDelete = useCallback(
(index: number) => {
const filteredChatHistory = aiChatHistory.filter((_, i) => i !== index);
setAiChatHistory(filteredChatHistory);
deleteChatMessage(filteredChatHistory);
},
[aiChatHistory],
);
const shouldShowQuickHelpPrompts =
message.length === 0 && aiChatHistory.length === 0;
const isDataLoading =
isLoading ||
isBillingDetailsLoading ||
isUserPersonaLoading ||
isUserResumeLoading;
useEffect(() => {
scrollToBottom('instant');
}, []);
const shouldShowUpgradeBanner = !isPaidUser && aiChatHistory.length > 0;
return (
<div className="ai-chat relative flex grow flex-col gap-2 bg-gray-100">
<div
className={cn(
'scrollbar-none absolute inset-0 overflow-y-auto pb-55',
shouldShowUpgradeBanner ? 'pb-60' : 'pb-55',
)}
ref={scrollableContainerRef}
>
<div className="relative mx-auto w-full max-w-3xl grow px-4">
{shouldShowQuickHelpPrompts && (
<QuickHelpPrompts
onQuestionClick={(question) => {
textareaMessageRef.current?.focus();
setMessage(question);
}}
/>
)}
{!shouldShowQuickHelpPrompts && (
<ChatHistory
chatHistory={aiChatHistory}
isStreamingMessage={isStreamingMessage}
streamedMessage={streamedMessage}
onDelete={handleDelete}
onRegenerate={handleRegenerate}
/>
)}
</div>
</div>
{isPersonalizedResponseFormOpen && (
<PersonalizedResponseForm
defaultValues={userPersona?.chatPreferences ?? undefined}
onClose={() => setIsPersonalizedResponseFormOpen(false)}
/>
)}
{isUploadResumeModalOpen && (
<UploadResumeModal
onClose={() => setIsUploadResumeModalOpen(false)}
userResume={userResume}
isUploading={isUploading}
uploadResume={uploadResume}
/>
)}
<div
className="pointer-events-none absolute right-0 bottom-0 left-0 mx-auto w-full max-w-3xl px-4"
ref={chatContainerRef}
>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<QuickActionButton
icon={PersonStandingIcon}
label="Personalize"
onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsPersonalizedResponseFormOpen(true);
}}
/>
<QuickActionButton
icon={FileUpIcon}
label={isUploading ? 'Processing...' : 'Upload Resume'}
onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsUploadResumeModalOpen(true);
}}
isLoading={isUploading}
/>
</div>
<div className="flex items-center gap-2">
{showScrollToBottomButton && (
<QuickActionButton
icon={ArrowDownIcon}
label="Scroll to Bottom"
onClick={scrollToBottom}
/>
)}
{aiChatHistory.length > 0 && !isPaidUser && (
<QuickActionButton
icon={TrashIcon}
label="Clear Chat"
onClick={() => {
setAiChatHistory([]);
deleteChatMessage([]);
}}
/>
)}
</div>
</div>
<form
className="pointer-events-auto relative flex flex-col gap-2 overflow-hidden rounded-lg rounded-b-none border border-b-0 border-gray-200 bg-white p-2.5"
onSubmit={(e) => {
e.preventDefault();
if (isDataLoading) {
return;
}
handleChatSubmit();
}}
>
<AutogrowTextarea
ref={textareaMessageRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
className="min-h-10 w-full resize-none bg-transparent text-sm focus:outline-none"
placeholder="Ask me anything..."
disabled={isStreamingMessage}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (isDataLoading) {
return;
}
e.preventDefault();
handleChatSubmit();
}
}}
/>
{isLimitExceeded && isLoggedIn() && !isDataLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon
className="size-4 cursor-not-allowed"
strokeWidth={2.5}
/>
<p className="cursor-not-allowed">
Limit reached for today
{isPaidUser ? '. Please wait until tomorrow.' : ''}
</p>
{!isPaidUser && (
<button
type="button"
onClick={() => {
onUpgrade?.();
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Upgrade for more
</button>
)}
</div>
)}
<div className="flex justify-end">
<button
type="submit"
className="flex size-8 shrink-0 items-center justify-center rounded-md border border-gray-200 disabled:cursor-not-allowed disabled:opacity-50"
disabled={isLimitExceeded || isStreamingMessage || isDataLoading}
>
<SendIcon className="size-4" />
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -1,51 +0,0 @@
import { Book } from 'lucide-react';
type AIChatCourseType = {
keyword: string;
difficulty: string;
};
function parseAIChatCourse(content: string): AIChatCourseType | null {
const courseKeywordRegex = /<keyword>(.*?)<\/keyword>/;
const courseKeyword = content.match(courseKeywordRegex)?.[1]?.trim();
if (!courseKeyword) {
return null;
}
const courseDifficultyRegex = /<difficulty>(.*?)<\/difficulty>/;
const courseDifficulty = content.match(courseDifficultyRegex)?.[1]?.trim();
if (!courseDifficulty) {
return null;
}
return { keyword: courseKeyword, difficulty: courseDifficulty || 'beginner' };
}
type AIChatCourseProps = {
content: string;
};
export function AIChatCourse(props: AIChatCourseProps) {
const { content } = props;
const course = parseAIChatCourse(content);
if (!course) {
return null;
}
const courseSearchUrl = `/ai/course?term=${course?.keyword}&difficulty=${course?.difficulty}`;
return (
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
<a
href={courseSearchUrl}
target="_blank"
key={course?.keyword}
className="group flex min-w-[120px] items-center gap-2 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-left text-sm text-gray-700 transition-all hover:border-gray-400 hover:text-black active:bg-gray-100"
>
<Book className="size-4 flex-shrink-0 text-gray-400" />
{course?.keyword}
</a>
</div>
);
}

View File

@@ -1,174 +0,0 @@
import { Fragment, memo } from 'react';
import { cn } from '../../lib/classname';
import {
CopyIcon,
CheckIcon,
TrashIcon,
type LucideIcon,
RotateCwIcon,
} from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text';
import { Tooltip } from '../Tooltip';
import type { RoadmapAIChatHistoryType } from '../../hooks/use-roadmap-ai-chat';
type ChatHistoryProps = {
chatHistory: RoadmapAIChatHistoryType[];
onDelete?: (index: number) => void;
onRegenerate?: (index: number) => void;
isStreamingMessage: boolean;
streamedMessage: React.ReactNode;
};
export const ChatHistory = memo((props: ChatHistoryProps) => {
const {
chatHistory,
onDelete,
isStreamingMessage,
streamedMessage,
onRegenerate,
} = props;
return (
<div className="flex grow flex-col">
<div className="relative flex grow flex-col justify-end">
<div className="flex grow flex-col justify-end gap-14 py-5">
{chatHistory.map((chat, index) => {
return (
<Fragment key={`chat-${index}`}>
<AIChatCard
{...chat}
onDelete={() => {
onDelete?.(index);
}}
onRegenerate={() => {
onRegenerate?.(index);
}}
/>
</Fragment>
);
})}
{isStreamingMessage && !streamedMessage && (
<AIChatCard
role="assistant"
content=""
html="<p>Thinking...</p>"
showActions={false}
/>
)}
{streamedMessage && (
<AIChatCard
role="assistant"
content=""
jsx={streamedMessage}
showActions={false}
/>
)}
</div>
</div>
</div>
);
});
type AIChatCardProps = RoadmapAIChatHistoryType & {
onDelete?: () => void;
onRegenerate?: () => void;
showActions?: boolean;
};
export const AIChatCard = memo((props: AIChatCardProps) => {
const {
role,
content,
jsx,
html,
showActions = true,
onDelete,
onRegenerate,
} = props;
const { copyText, isCopied } = useCopyText();
return (
<div
className={cn(
'group/content relative flex w-full flex-col',
role === 'user' ? 'items-end' : 'items-start',
)}
>
<div
className={cn(
'flex max-w-full items-start gap-2.5 rounded-lg',
role === 'user' ? 'max-w-[70%] bg-gray-200 p-3' : 'w-full',
)}
>
{!!jsx && jsx}
{!!html && (
<div
className="course-content course-ai-content prose prose-sm overflow-hidden text-sm"
dangerouslySetInnerHTML={{ __html: html }}
/>
)}
</div>
{showActions && (
<div
className={cn(
'absolute -bottom-2 flex translate-y-full items-center gap-1',
role === 'user' ? 'right-0' : 'left-0',
)}
>
<ActionButton
icon={isCopied ? CheckIcon : CopyIcon}
onClick={() => copyText(content ?? '')}
tooltip={isCopied ? 'Copied' : 'Copy'}
/>
{role === 'assistant' && onRegenerate && (
<ActionButton
icon={RotateCwIcon}
onClick={onRegenerate}
tooltip="Regenerate"
/>
)}
{onDelete && (
<ActionButton
icon={TrashIcon}
onClick={onDelete}
tooltip="Delete"
/>
)}
</div>
)}
</div>
);
});
type ActionButtonProps = {
icon: LucideIcon;
tooltip?: string;
onClick: () => void;
};
function ActionButton(props: ActionButtonProps) {
const { icon: Icon, onClick, tooltip } = props;
return (
<div className="group relative">
<button
className="flex size-8 items-center justify-center rounded-lg opacity-0 transition-opacity group-hover/content:opacity-100 hover:bg-gray-200"
onClick={onClick}
>
<Icon className="size-4 stroke-[2.5]" />
</button>
{tooltip && (
<Tooltip position="top-center" additionalClass="-translate-y-1">
{tooltip}
</Tooltip>
)}
</div>
);
}

View File

@@ -1,240 +0,0 @@
import { Loader2Icon } from 'lucide-react';
import { MessageCircle } from 'lucide-react';
import { memo, useId, useRef, useState } from 'react';
import { Modal } from '../Modal';
import { cn } from '../../lib/classname';
import { SelectNative } from '../SelectNative';
import { useMutation } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import { userPersonaOptions } from '../../queries/user-persona';
import { useToast } from '../../hooks/use-toast';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
export type ChatPreferencesFormData = {
expertise: string;
goal: string;
about: string;
specialInstructions?: string;
};
type PersonalizedResponseFormProps = {
defaultValues?: ChatPreferencesFormData;
onClose: () => void;
};
export const PersonalizedResponseForm = memo(
(props: PersonalizedResponseFormProps) => {
const { defaultValues, onClose } = props;
const toast = useToast();
const [expertise, setExpertise] = useState(defaultValues?.expertise ?? '');
const [about, setAbout] = useState(defaultValues?.about ?? '');
const [specialInstructions, setSpecialInstructions] = useState(
defaultValues?.specialInstructions ?? ''
);
const goalOptions = [
'Finding a job',
'Learning for fun',
'Building a side project',
'Switching careers',
'Getting a promotion',
'Filling knowledge gaps',
'Other',
];
const getInitialGoalSelection = () => {
if (!defaultValues?.goal) {
return '';
}
for (const option of goalOptions.slice(0, -1)) {
if (defaultValues.goal.startsWith(option)) {
return option;
}
}
return 'Other';
};
const [selectedGoal, setSelectedGoal] = useState(getInitialGoalSelection());
const [goal, setGoal] = useState(defaultValues?.goal ?? '');
const expertiseFieldId = useId();
const goalFieldId = useId();
const goalSelectId = useId();
const aboutFieldId = useId();
const specialInstructionsFieldId = useId();
const goalRef = useRef<HTMLTextAreaElement>(null);
const handleGoalSelectionChange = (value: string) => {
setSelectedGoal(value);
if (value === 'Other') {
setGoal('');
setTimeout(() => {
goalRef.current?.focus();
}, 0);
} else {
setGoal(value);
}
};
const { mutate: setChatPreferences, isPending } = useMutation(
{
mutationFn: (data: ChatPreferencesFormData) => {
return httpPost('/v1-set-chat-preferences', data);
},
onSuccess: () => {
onClose();
queryClient.invalidateQueries(userPersonaOptions());
},
onError: (error) => {
toast.error(error?.message ?? 'Something went wrong');
},
},
queryClient
);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setChatPreferences({
expertise,
goal,
about,
specialInstructions,
});
};
const hasFormCompleted = !!expertise && !!goal && !!about;
return (
<Modal onClose={onClose}>
<div className="p-4">
<form onSubmit={handleSubmit} className={cn('space-y-8')}>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={expertiseFieldId}
>
Rate your Experience
</label>
<SelectNative
id={expertiseFieldId}
value={expertise}
defaultValue={expertise}
onChange={(e) => setExpertise(e.target.value)}
className="h-[40px] border-gray-300 text-sm focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
>
<option value="">Select your expertise</option>
{[
'No experience (just starting out)',
'Beginner (less than 1 year of experience)',
'Intermediate (1-3 years of experience)',
'Expert (3-5 years of experience)',
'Master (5+ years of experience)',
].map((expertise) => (
<option key={expertise} value={expertise}>
{expertise}
</option>
))}
</SelectNative>
</div>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={goalSelectId}
>
What is your goal?
</label>
<SelectNative
id={goalSelectId}
value={selectedGoal}
onChange={(e) => handleGoalSelectionChange(e.target.value)}
className="h-[40px] border-gray-300 text-sm focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
>
<option value="">Select your goal</option>
{goalOptions.map((goalOption) => (
<option key={goalOption} value={goalOption}>
{goalOption}
</option>
))}
</SelectNative>
{selectedGoal === 'Other' && (
<textarea
ref={goalRef}
id={goalFieldId}
className="block min-h-24 w-full resize-none rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm outline-none placeholder:text-gray-400 focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
placeholder="e.g. need to find a job as soon as possible"
value={goal}
onChange={(e) => setGoal(e.target.value)}
/>
)}
</div>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={aboutFieldId}
>
Tell us more about yourself
</label>
<textarea
id={aboutFieldId}
className="block min-h-24 w-full resize-none rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm outline-none placeholder:text-gray-400 focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
placeholder="e.g. I'm a software engineer with 5 years of experience"
value={about}
onChange={(e) => setAbout(e.target.value)}
/>
</div>
<div className="flex flex-col gap-3">
<label
className="text-sm font-medium text-gray-700"
htmlFor={specialInstructionsFieldId}
>
Special Instructions
</label>
<textarea
id={specialInstructionsFieldId}
className="block min-h-24 w-full resize-none rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm outline-none placeholder:text-gray-400 focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
placeholder="e.g. Prefer concise responses with code examples"
value={specialInstructions}
onChange={(e) => setSpecialInstructions(e.target.value)}
/>
</div>
<button
disabled={!hasFormCompleted || isPending}
type="submit"
className="mt-6 flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-black px-6 py-2 text-sm text-white transition-all hover:bg-gray-900 disabled:pointer-events-none disabled:opacity-50"
>
{isPending ? (
<Loader2Icon className="size-4 animate-spin stroke-[2.5]" />
) : (
<>
<MessageCircle className="size-4" />
{defaultValues ? 'Update Preferences' : 'Set Preferences'}
</>
)}
</button>
</form>
</div>
</Modal>
);
}
);

View File

@@ -1,29 +0,0 @@
import { Loader2Icon, type LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
type QuickActionButtonProps = {
icon?: LucideIcon;
label?: string;
onClick?: () => void;
className?: string;
isLoading?: boolean;
};
export function QuickActionButton(props: QuickActionButtonProps) {
const { icon: Icon, label, onClick, className, isLoading } = props;
return (
<button
className={cn(
'pointer-events-auto flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1.5 text-sm text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
onClick={onClick}
disabled={isLoading}
>
{Icon && !isLoading && <Icon className="size-4" />}
{isLoading && Icon && <Loader2Icon className="size-4 animate-spin" />}
<span className="hidden lg:block">{label}</span>
</button>
);
}

View File

@@ -1,88 +0,0 @@
import { useState } from 'react';
import { cn } from '../../lib/classname';
type QuickHelpPromptsProps = {
onQuestionClick: (question: string) => void;
};
export function QuickHelpPrompts(props: QuickHelpPromptsProps) {
const { onQuestionClick } = props;
const [selectedActionIndex, setSelectedActionIndex] = useState<number>(0);
const quickActions = [
{
label: 'Help select a career path',
questions: [
'What roadmap should I pick?',
'What are the best jobs for me?',
'Recommend me a project based on my expertise',
'Recommend me a topic I can learn in an hour',
],
},
{
label: 'Help me find a job',
questions: [
'How can I improve my resume?',
'How to make a tech resume?',
'Whats asked in coding interviews?',
'Where to find remote dev jobs?',
],
},
{
label: 'Learn a Topic',
questions: [
'What is the best way to learn React?',
'What is an API?',
'How do databases work?',
'What is async in JS?',
],
},
{
label: 'Test my Knowledge',
questions: [
'Quiz me on arrays.',
'Test my SQL skills.',
'Ask about REST basics.',
'Test my JS async knowledge.',
],
},
];
const selectedAction = quickActions[selectedActionIndex];
return (
<div className="mt-24">
<h2 className="text-2xl font-semibold">How can I help you?</h2>
<div className="mt-6 flex flex-wrap items-center gap-2">
{quickActions.map((action, index) => (
<button
key={action.label}
className={cn(
'pointer-events-auto flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-white px-2 py-1.5 text-sm hover:bg-gray-100 hover:text-black',
selectedActionIndex === index
? 'border-gray-300 bg-white text-black hover:bg-white'
: 'border-gray-300 bg-gray-100 text-gray-500 hover:border-gray-300 hover:bg-gray-50',
)}
onClick={() => setSelectedActionIndex(index)}
>
{action.label}
</button>
))}
</div>
<div className="mt-6 divide-y divide-gray-200">
{selectedAction.questions.map((question) => (
<button
type="button"
key={question}
className="block w-full cursor-pointer p-2 text-left text-sm text-gray-500 hover:bg-gray-100 hover:text-black"
onClick={() => onQuestionClick(question)}
>
{question}
</button>
))}
</div>
</div>
);
}

View File

@@ -1,223 +0,0 @@
import { useCallback, useState, type FormEvent } from 'react';
import { Modal } from '../Modal';
import {
useDropzone,
type DropEvent,
type FileRejection,
} from 'react-dropzone';
import { cn } from '../../lib/classname';
import { Loader2Icon, PlusIcon, XIcon } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { httpDelete } from '../../lib/query-http';
import { useToast } from '../../hooks/use-toast';
import { queryClient } from '../../stores/query-client';
import {
userResumeOptions,
type UserResumeDocument,
} from '../../queries/user-resume';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
type OnDrop<T extends File = File> = (
acceptedFiles: T[],
fileRejections: FileRejection[],
event: DropEvent,
) => void;
type UploadResumeModalProps = {
userResume?: UserResumeDocument;
onClose: () => void;
isUploading: boolean;
uploadResume: (formData: FormData) => void;
};
export function UploadResumeModal(props: UploadResumeModalProps) {
const {
onClose,
userResume: defaultUserResume,
isUploading,
uploadResume,
} = props;
const toast = useToast();
const [showLinkedInExport, setShowLinkedInExport] = useState(false);
const [file, setFile] = useState<File | null>(
defaultUserResume?.resumeUrl
? new File([], defaultUserResume.fileName, {
type: defaultUserResume.fileType,
})
: null,
);
const onDrop: OnDrop = useCallback((acceptedFiles) => {
setFile(acceptedFiles[0]);
}, []);
const { mutate: deleteResume, isPending: isDeletingResume } = useMutation(
{
mutationFn: async () => {
return httpDelete('/v1-delete-resume');
},
onSuccess: () => {
setFile(null);
},
onSettled: () => {
return queryClient.invalidateQueries(userResumeOptions());
},
onError: (error) => {
toast.error(error?.message || 'Failed to delete resume');
},
},
queryClient,
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf'],
},
maxFiles: 1,
maxSize: 5 * 1024 * 1024, // 5MB
});
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (!file) {
return;
}
const formData = new FormData();
formData.append('resume', file);
uploadResume(formData);
};
const size = file?.size || defaultUserResume?.fileSize || 0;
const fileSize = (size / 1024 / 1024).toFixed(2);
return (
<Modal onClose={onClose}>
{showLinkedInExport ? (
<div className="p-4 pt-8">
<h2 className="text-center text-2xl font-semibold text-black">
How to export LinkedIn Resume
</h2>
<p className="mt-2 text-center text-sm text-balance text-gray-500">
Visit your LinkedIn profile and export your resume as a PDF.
</p>
<img
src="https://assets.roadmap.sh/guest/linkedin-resume-export-w3x2f.png"
alt="LinkedIn Resume Export"
className="mt-6 min-h-[331px] rounded-xl object-cover"
/>
<button
onClick={() => setShowLinkedInExport(false)}
className="mt-4 flex w-full cursor-pointer items-center justify-center rounded-lg bg-black p-1 py-3 leading-none tracking-wide text-white transition-colors hover:bg-gray-900"
>
Back to Upload
</button>
</div>
) : (
<form
className="p-4 pt-8"
encType="multipart/form-data"
onSubmit={handleSubmit}
>
<h2 className="text-center text-2xl font-semibold text-black">
Upload your resume
</h2>
<p className="mt-2 text-center text-sm text-balance text-gray-500">
Upload your resume to get personalized responses to your questions.
</p>
{file && (
<div className="mt-8">
<div className="flex items-center justify-between gap-2 rounded-lg border border-gray-200 p-4">
<div>
<h3 className="text-base font-medium text-black">
{file.name}
</h3>
<p className="mt-0.5 text-sm text-gray-500">{fileSize} MB</p>
</div>
<button
type="button"
className="flex size-8 items-center justify-center rounded-md text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
disabled={isDeletingResume}
onClick={() => deleteResume()}
>
{isDeletingResume ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<XIcon className="size-4" />
)}
</button>
</div>
</div>
)}
{!file && (
<>
<div
{...getRootProps({
className: cn(
'border border-dashed border-gray-300 min-h-60 flex items-center justify-center rounded-lg p-4 mt-8 bg-gray-50 cursor-pointer hover:border-black transition-colors',
isDragActive && 'border-black bg-gray-100',
),
})}
>
<input {...getInputProps()} />
<div className="mx-auto flex max-w-2xs flex-col items-center text-center text-balance">
<PlusIcon className="size-5 text-gray-500" />
<p className="mt-4 text-gray-600">
Drag and drop your resume here or{' '}
<span className="font-semibold text-black">
click to browse
</span>
</p>
</div>
</div>
<p className="mt-4 text-center text-xs text-gray-500">
Only PDF files (max 2MB in size) are supported
</p>
</>
)}
{!defaultUserResume && (
<>
<button
type="submit"
className="mt-4 flex w-full cursor-pointer items-center justify-center rounded-lg bg-black p-1 py-3 leading-none tracking-wide text-white transition-colors hover:bg-gray-900 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-[loading=true]:cursor-wait"
data-loading={String(isUploading)}
disabled={!file || isUploading || isDeletingResume}
>
{isUploading ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
'Upload Resume'
)}
</button>
<p className="mt-4 text-center text-xs text-gray-500">
You can also export your resume from{' '}
<button
type="button"
onClick={() => setShowLinkedInExport(true)}
className="text-black underline underline-offset-2 hover:text-gray-600"
>
LinkedIn
</button>
</p>
</>
)}
</form>
)}
</Modal>
);
}

View File

@@ -1,171 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { chatHistoryOptions } from '../../queries/chat-history';
import { AIChat, aiChatRenderer } from '../AIChat/AIChat';
import { Loader2Icon } from 'lucide-react';
import { useEffect, useState, useCallback } from 'react';
import { AIChatLayout } from './AIChatLayout';
import { ListChatHistory } from './ListChatHistory';
import { billingDetailsOptions } from '../../queries/billing';
import { ChatHistoryError } from './ChatHistoryError';
import { useClientMount } from '../../hooks/use-client-mount';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type AIChatHistoryProps = {
chatHistoryId?: string;
};
export function AIChatHistory(props: AIChatHistoryProps) {
const { chatHistoryId: defaultChatHistoryId } = props;
const isClientMounted = useClientMount();
const [keyTrigger, setKeyTrigger] = useState(0);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [isChatHistoryLoading, setIsChatHistoryLoading] = useState(true);
const [chatHistoryId, setChatHistoryId] = useState<string | undefined>(
defaultChatHistoryId || undefined,
);
const { data, error: chatHistoryError } = useQuery(
chatHistoryOptions(chatHistoryId, aiChatRenderer),
queryClient,
);
const {
data: userBillingDetails,
isLoading: isBillingDetailsLoading,
error: billingDetailsError,
} = useQuery(billingDetailsOptions(), queryClient);
const handleChatHistoryClick = useCallback(
(nextChatHistoryId: string | null) => {
setKeyTrigger((key) => key + 1);
if (nextChatHistoryId === null) {
setChatHistoryId(undefined);
window.history.replaceState(null, '', '/ai/chat');
return;
}
// show loader only if the chat history hasn't been fetched before (avoids UI flash)
const hasAlreadyFetched = queryClient.getQueryData(
chatHistoryOptions(nextChatHistoryId).queryKey,
);
if (!hasAlreadyFetched) {
setIsChatHistoryLoading(true);
}
setChatHistoryId(nextChatHistoryId);
window.history.replaceState(null, '', `/ai/chat/${nextChatHistoryId}`);
},
[],
);
const handleDelete = useCallback(
(deletedChatHistoryId: string) => {
if (deletedChatHistoryId !== chatHistoryId) {
return;
}
setChatHistoryId(undefined);
window.history.replaceState(null, '', '/ai/chat');
setKeyTrigger((key) => key + 1);
},
[chatHistoryId],
);
const isPaidUser = userBillingDetails?.status === 'active';
const hasError = chatHistoryError || billingDetailsError;
const showLoader = isChatHistoryLoading && !hasError;
const showError = !isChatHistoryLoading && Boolean(hasError);
useEffect(() => {
if (!chatHistoryId) {
setIsChatHistoryLoading(false);
return;
}
if (!data) {
return;
}
setIsChatHistoryLoading(false);
}, [data, chatHistoryId]);
useEffect(() => {
if (!hasError) {
return;
}
setIsChatHistoryLoading(false);
}, [hasError]);
if (!isClientMounted || isBillingDetailsLoading) {
return (
<AIChatLayout>
<div className="relative flex grow">
<div className="absolute inset-0 z-20 flex items-center justify-center">
<Loader2Icon className="h-8 w-8 animate-spin stroke-[2.5] text-gray-400/80" />
</div>
</div>
</AIChatLayout>
);
}
return (
<AIChatLayout>
<div className="relative flex grow">
<ListChatHistory
activeChatHistoryId={chatHistoryId}
onChatHistoryClick={handleChatHistoryClick}
onDelete={handleDelete}
isPaidUser={isPaidUser}
onUpgrade={() => {
setShowUpgradeModal(true);
}}
/>
<div className="relative flex grow">
{showLoader && (
<div className="absolute inset-0 z-20 flex items-center justify-center">
<Loader2Icon className="h-8 w-8 animate-spin stroke-[2.5] text-gray-400/80" />
</div>
)}
{showError && (
<div className="absolute inset-0 z-20 flex items-center justify-center">
<ChatHistoryError error={hasError} className="mt-0" />
</div>
)}
{!showLoader && !showError && (
<AIChat
key={keyTrigger}
messages={data?.messages}
chatHistoryId={chatHistoryId}
setChatHistoryId={(id) => {
setChatHistoryId(id);
window.history.replaceState(null, '', `/ai/chat/${id}`);
queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'list-chat-history';
},
});
}}
onUpgrade={() => {
setShowUpgradeModal(true);
}}
/>
)}
</div>
</div>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
</AIChatLayout>
);
}

View File

@@ -1,22 +0,0 @@
import { AITutorLayout } from '../AITutor/AITutorLayout';
import { CheckSubscriptionVerification } from '../Billing/CheckSubscriptionVerification';
import { Loader2Icon } from 'lucide-react';
type AIChatLayoutProps = {
children: React.ReactNode;
};
export function AIChatLayout(props: AIChatLayoutProps) {
const { children } = props;
return (
<AITutorLayout
activeTab="chat"
wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden"
containerClassName="h-[calc(100vh-49px)] overflow-hidden"
>
{children}
<CheckSubscriptionVerification />
</AITutorLayout>
);
}

View File

@@ -1,116 +0,0 @@
import { EllipsisVerticalIcon, Loader2Icon, Trash2Icon } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../DropdownMenu';
import { queryClient } from '../../stores/query-client';
import { useMutation } from '@tanstack/react-query';
import { httpDelete } from '../../lib/query-http';
import { listChatHistoryOptions } from '../../queries/chat-history';
import { useState } from 'react';
import { useToast } from '../../hooks/use-toast';
type ChatHistoryActionProps = {
chatHistoryId: string;
onDelete?: () => void;
};
export function ChatHistoryAction(props: ChatHistoryActionProps) {
const { chatHistoryId, onDelete } = props;
const toast = useToast();
const [isOpen, setIsOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const { mutate: deleteChatHistory, isPending: isDeletingLoading } =
useMutation(
{
mutationFn: (chatHistoryId: string) => {
return httpDelete(`/v1-delete-chat/${chatHistoryId}`);
},
onSettled: () => {
return queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'list-chat-history';
},
});
},
onSuccess: () => {
toast.success('Chat history deleted');
setIsOpen(false);
onDelete?.();
},
onError: (error) => {
toast.error(error?.message || 'Failed to delete chat history');
},
},
queryClient,
);
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger className="rounded-lg p-2 opacity-0 group-hover/item:opacity-100 hover:bg-gray-100 focus:outline-none data-[state=open]:bg-gray-100 data-[state=open]:opacity-100">
<EllipsisVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="z-[9999]">
{!isDeleting && (
<DropdownMenuItem
className="cursor-pointer text-red-500 focus:bg-red-50 focus:text-red-500"
onSelect={(e) => {
e.preventDefault();
setIsDeleting(true);
}}
disabled={isDeletingLoading}
>
{isDeletingLoading ? (
<>
<Loader2Icon className="h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2Icon className="h-4 w-4" />
Delete
</>
)}
</DropdownMenuItem>
)}
{isDeleting && (
<DropdownMenuItem
asChild
className="focus:bg-transparent"
onSelect={(e) => {
e.preventDefault();
}}
disabled={isDeletingLoading}
>
<div className="flex w-full items-center justify-between gap-1.5">
Are you sure?
<div className="flex items-center gap-2">
<button
onClick={() => {
deleteChatHistory(chatHistoryId);
setIsDeleting(false);
}}
className="cursor-pointer text-red-500 underline hover:text-red-800"
disabled={isDeletingLoading}
>
Yes
</button>
<button
onClick={() => setIsDeleting(false)}
className="cursor-pointer text-red-500 underline hover:text-red-800"
disabled={isDeletingLoading}
>
No
</button>
</div>
</div>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,28 +0,0 @@
import { AlertCircleIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
type ChatHistoryErrorProps = {
error: Error | null;
className?: string;
};
export function ChatHistoryError(props: ChatHistoryErrorProps) {
const { error, className } = props;
return (
<div
className={cn(
'mt-10 flex max-w-md flex-col items-center justify-center text-center',
className,
)}
>
<AlertCircleIcon className="h-8 w-8 text-red-500" />
<h3 className="mt-4 text-sm font-medium text-gray-900">
Something went wrong
</h3>
<p className="mt-0.5 text-xs text-balance text-gray-500">
{error?.message}
</p>
</div>
);
}

View File

@@ -1,42 +0,0 @@
import type { ChatHistoryWithoutMessages } from '../../queries/chat-history';
import { ChatHistoryItem } from './ChatHistoryItem';
type ChatHistoryGroupProps = {
title: string;
histories: ChatHistoryWithoutMessages[];
activeChatHistoryId?: string;
onChatHistoryClick: (id: string) => void;
onDelete: (id: string) => void;
};
export function ChatHistoryGroup(props: ChatHistoryGroupProps) {
const {
title,
histories,
activeChatHistoryId,
onChatHistoryClick,
onDelete,
} = props;
return (
<div>
<h2 className="ml-2 text-xs text-gray-500">{title}</h2>
<ul className="mt-1 space-y-0.5">
{histories.map((chatHistory) => {
return (
<ChatHistoryItem
key={chatHistory._id}
chatHistory={chatHistory}
isActive={activeChatHistoryId === chatHistory._id}
onChatHistoryClick={onChatHistoryClick}
onDelete={() => {
onDelete?.(chatHistory._id);
}}
/>
);
})}
</ul>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import { cn } from '../../lib/classname';
import type { ChatHistoryDocument } from '../../queries/chat-history';
import { ChatHistoryAction } from './ChatHistoryAction';
type ChatHistoryItemProps = {
chatHistory: Omit<ChatHistoryDocument, 'messages'>;
isActive: boolean;
onChatHistoryClick: (chatHistoryId: string) => void;
onDelete?: () => void;
};
export function ChatHistoryItem(props: ChatHistoryItemProps) {
const { chatHistory, isActive, onChatHistoryClick, onDelete } = props;
return (
<li key={chatHistory._id} className="group/item relative text-sm">
<button
className="block w-full truncate rounded-lg p-2 pr-10 text-left hover:bg-gray-100 data-[active=true]:bg-gray-100"
data-active={isActive}
onClick={() => onChatHistoryClick(chatHistory._id)}
>
{chatHistory.title}
</button>
<div className="absolute inset-y-0 right-2 flex items-center">
<ChatHistoryAction
chatHistoryId={chatHistory._id}
onDelete={onDelete}
/>
</div>
</li>
);
}

View File

@@ -1,292 +0,0 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { listChatHistoryOptions } from '../../queries/chat-history';
import { queryClient } from '../../stores/query-client';
import {
Loader2Icon,
LockIcon,
PanelLeftCloseIcon,
PanelLeftIcon,
PlusIcon,
} from 'lucide-react';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { ListChatHistorySkeleton } from './ListChatHistorySkeleton';
import { ChatHistoryError } from './ChatHistoryError';
import { cn } from '../../lib/classname';
import { getTailwindScreenDimension } from '../../lib/is-mobile';
import { groupChatHistory } from '../../helper/grouping';
import { SearchAIChatHistory } from './SearchAIChatHistory';
import { ChatHistoryGroup } from './ChatHistoryGroup';
import { isLoggedIn } from '../../lib/jwt';
import { CheckIcon } from '../ReactIcons/CheckIcon';
type ListChatHistoryProps = {
activeChatHistoryId?: string;
onChatHistoryClick: (chatHistoryId: string | null) => void;
onDelete?: (chatHistoryId: string) => void;
isPaidUser?: boolean;
onUpgrade?: () => void;
};
export function ListChatHistory(props: ListChatHistoryProps) {
const {
activeChatHistoryId,
onChatHistoryClick,
onDelete,
isPaidUser,
onUpgrade,
} = props;
const [isOpen, setIsOpen] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const [isMobile, setIsMobile] = useState(false);
useLayoutEffect(() => {
const deviceType = getTailwindScreenDimension();
const isMediumSize = ['sm', 'md'].includes(deviceType);
// Only set initial state from localStorage if not on mobile
if (!isMediumSize) {
const storedState = localStorage.getItem('chat-history-sidebar-open');
setIsOpen(storedState === null ? true : storedState === 'true');
} else {
setIsOpen(!isMediumSize);
}
setIsMobile(isMediumSize);
}, []);
// Save state to localStorage when it changes, but only if not on mobile
useEffect(() => {
if (!isMobile) {
localStorage.setItem('chat-history-sidebar-open', isOpen.toString());
}
}, [isOpen, isMobile]);
const [query, setQuery] = useState('');
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isError,
error,
isLoading: isLoadingInfiniteQuery,
} = useInfiniteQuery(listChatHistoryOptions({ query }), queryClient);
useEffect(() => {
if (!data) {
return;
}
setIsLoading(false);
}, [data?.pages]);
const groupedChatHistory = useMemo(() => {
const allHistories = data?.pages?.flatMap((page) => page.data);
return groupChatHistory(allHistories ?? []);
}, [data?.pages]);
if (!isLoggedIn()) {
return null;
}
if (!isOpen) {
return (
<div className="absolute top-2 left-2 z-20 flex items-center gap-1">
<button
className="flex size-8 items-center justify-center rounded-lg p-1 hover:bg-gray-200"
onClick={() => {
setIsOpen(true);
}}
>
<PanelLeftIcon className="h-4.5 w-4.5" />
</button>
<button
className="flex size-8 items-center justify-center rounded-lg p-1 hover:bg-gray-200"
onClick={() => {
if (isMobile) {
setIsOpen(false);
}
onChatHistoryClick(null);
}}
>
<PlusIcon className="h-4.5 w-4.5" />
</button>
</div>
);
}
const isEmptyHistory = Object.values(groupedChatHistory ?? {}).every(
(group) => group.histories.length === 0,
);
const classNames = cn(
'flex w-[255px] shrink-0 flex-col justify-start border-r border-gray-200 bg-white p-2',
'max-md:absolute max-md:inset-0 max-md:z-20 max-md:w-full',
!isOpen && 'hidden',
);
const closeButton = (
<button
className="flex size-8 items-center justify-center rounded-lg p-1 text-gray-500 hover:bg-gray-100 hover:text-black"
onClick={() => {
setIsOpen(false);
}}
>
<PanelLeftCloseIcon className="h-4.5 w-4.5" />
</button>
);
if (!isPaidUser) {
return (
<UpgradeToProMessage
className={classNames}
closeButton={closeButton}
onUpgrade={onUpgrade}
/>
);
}
return (
<div className={classNames}>
{isLoading && <ListChatHistorySkeleton />}
{!isLoading && isError && <ChatHistoryError error={error} />}
{!isLoading && !isError && (
<>
<div>
<div className="mb-4 flex items-center justify-between">
<h1 className="font-medium text-gray-900">Chat History</h1>
{closeButton}
</div>
<button
className="flex w-full items-center justify-center gap-2 rounded-lg bg-black p-2 text-sm text-white hover:opacity-80"
onClick={() => {
if (isMobile) {
setIsOpen(false);
}
onChatHistoryClick(null);
}}
>
<PlusIcon className="h-4 w-4" />
<span className="text-sm">New Chat</span>
</button>
<SearchAIChatHistory
onSearch={setQuery}
isLoading={isLoadingInfiniteQuery}
/>
</div>
<div className="scrollbar-track-transparent scrollbar-thin scrollbar-thumb-gray-300 -mx-2 mt-6 grow space-y-4 overflow-y-scroll px-2">
{isEmptyHistory && !isLoadingInfiniteQuery && (
<div className="flex items-center justify-center">
<p className="text-sm text-gray-500">No chat history</p>
</div>
)}
{Object.entries(groupedChatHistory ?? {}).map(([key, value]) => {
if (value.histories.length === 0) {
return null;
}
return (
<ChatHistoryGroup
key={key}
title={value.title}
histories={value.histories}
activeChatHistoryId={activeChatHistoryId}
onChatHistoryClick={(id) => {
if (isMobile) {
setIsOpen(false);
}
onChatHistoryClick(id);
}}
onDelete={(id) => {
onDelete?.(id);
}}
/>
);
})}
{hasNextPage && (
<div className="mt-4">
<button
className="flex w-full items-center justify-center gap-2 text-sm text-gray-500 hover:text-black"
onClick={() => {
fetchNextPage();
}}
disabled={isFetchingNextPage}
>
{isFetchingNextPage && (
<>
<Loader2Icon className="h-4 w-4 animate-spin" />
Loading more...
</>
)}
{!isFetchingNextPage && 'Load More'}
</button>
</div>
)}
</div>
</>
)}
</div>
);
}
type UpgradeToProMessageProps = {
className?: string;
onUpgrade?: () => void;
closeButton?: React.ReactNode;
};
export function UpgradeToProMessage(props: UpgradeToProMessageProps) {
const { className, onUpgrade, closeButton } = props;
return (
<div className={cn('relative flex flex-col', className)}>
<div className="mb-4 flex items-center justify-between">
{closeButton}
</div>
<div className="flex grow flex-col items-center justify-center px-4">
<div className="flex flex-col items-center">
<div className="mb-3 rounded-full bg-yellow-100 p-3">
<LockIcon className="size-6 text-yellow-600" />
</div>
<h2 className="text-lg font-semibold text-gray-900">
Unlock History
</h2>
<p className="mt-2 text-center text-sm text-balance text-gray-600">
Save conversations and pick up right where you left off.
</p>
</div>
<div className="my-5 w-full space-y-2">
<div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
<CheckIcon additionalClasses="size-4 text-green-500" />
<span className="text-sm text-gray-600">Unlimited history</span>
</div>
<div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
<CheckIcon additionalClasses="size-4 text-green-500" />
<span className="text-sm text-gray-600">Search old chats</span>
</div>
</div>
<button
type="button"
className="w-full cursor-pointer rounded-lg bg-yellow-400 px-4 py-2 text-sm font-medium text-black hover:bg-yellow-500"
onClick={() => {
onUpgrade?.();
}}
>
Upgrade to Pro
</button>
</div>
</div>
);
}

View File

@@ -1,35 +0,0 @@
export function ListChatHistorySkeleton() {
return (
<>
<div>
<div className="mb-4 flex items-center justify-between gap-2">
<div className="h-6 w-1/2 animate-pulse rounded bg-gray-200" />
<div className="size-8 animate-pulse rounded-md bg-gray-200" />
</div>
<div className="h-9 w-full animate-pulse rounded-lg bg-gray-200" />
<div className="relative mt-2">
<div className="h-9 w-full animate-pulse rounded-lg bg-gray-200" />
</div>
</div>
<div className="scrollbar-track-transparent scrollbar-thin scrollbar-thumb-gray-300 -mx-2 mt-6 grow space-y-4 overflow-y-scroll px-2">
{['Today', 'Last 7 Days', 'Older'].map((group) => (
<div key={group}>
<div className="h-4 w-16 animate-pulse rounded bg-gray-200" />
<ul className="mt-1 space-y-0.5">
{[1, 2, 3].map((i) => (
<li
key={i}
className="h-9 animate-pulse rounded-lg bg-gray-100"
></li>
))}
</ul>
</div>
))}
</div>
</>
);
}

View File

@@ -1,66 +0,0 @@
import { useEffect, useState } from 'react';
import { useDebounceValue } from '../../hooks/use-debounce';
import { Loader2Icon, XIcon, SearchIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
type SearchAIChatHistoryProps = {
onSearch: (search: string) => void;
isLoading?: boolean;
className?: string;
inputClassName?: string;
};
export function SearchAIChatHistory(props: SearchAIChatHistoryProps) {
const { onSearch, isLoading, className, inputClassName } = props;
const [search, setSearch] = useState('');
const debouncedSearch = useDebounceValue(search, 300);
useEffect(() => {
onSearch(debouncedSearch);
}, [debouncedSearch, onSearch]);
return (
<form
className={cn('relative mt-2 flex grow items-center', className)}
onSubmit={(e) => {
e.preventDefault();
onSearch(search);
}}
>
<input
type="text"
placeholder="Search folder by name"
className={cn(
'block h-9 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pr-7 pl-8 text-sm outline-none placeholder:text-zinc-500 focus:border-zinc-500',
inputClassName,
)}
required
minLength={3}
maxLength={255}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute top-1/2 left-2.5 -translate-y-1/2">
{isLoading ? (
<Loader2Icon className="size-4 animate-spin text-gray-500" />
) : (
<SearchIcon className="size-4 text-gray-500" />
)}
</div>
{search && (
<div className="absolute inset-y-0 right-1 flex items-center">
<button
onClick={() => {
setSearch('');
}}
className="rounded-lg p-1 hover:bg-gray-100"
>
<XIcon className="size-4 text-gray-500" />
</button>
</div>
)}
</form>
);
}

View File

@@ -1,116 +0,0 @@
import { ArrowUpRightIcon, MoreVertical, Play, Trash2 } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import { useToast } from '../../hooks/use-toast';
import { useMutation } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { httpDelete } from '../../lib/query-http';
type AIGuideActionsType = {
guideSlug: string;
onDeleted?: () => void;
};
export function AIGuideActions(props: AIGuideActionsType) {
const { guideSlug, onDeleted } = props;
const toast = useToast();
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const { mutate: deleteCourse, isPending: isDeleting } = useMutation(
{
mutationFn: async () => {
return httpDelete(`/v1-delete-ai-guide/${guideSlug}`);
},
onSuccess: () => {
toast.success('Guide deleted');
queryClient.invalidateQueries({
predicate: (query) => query.queryKey?.[0] === 'user-ai-guides',
});
onDeleted?.();
},
onError: (error) => {
toast.error(error?.message || 'Failed to delete guide');
},
},
queryClient,
);
useOutsideClick(dropdownRef, () => {
setIsOpen(false);
});
useKeydown('Escape', () => {
setIsOpen(false);
});
return (
<div className="relative h-full" ref={dropdownRef}>
<button
className="h-full text-gray-400 hover:text-gray-700"
onClick={(e) => {
e.stopPropagation();
setIsOpen(!isOpen);
}}
>
<MoreVertical size={16} />
</button>
{isOpen && (
<div className="absolute top-8 right-0 z-10 w-48 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
<a
href={`/ai/guide/${guideSlug}`}
className="flex w-full items-center gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70"
>
<ArrowUpRightIcon className="h-3.5 w-3.5" />
View Guide
</a>
{!isConfirming && (
<button
className="flex w-full items-center gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70"
onClick={() => setIsConfirming(true)}
disabled={isDeleting}
>
{!isDeleting ? (
<>
<Trash2 className="h-3.5 w-3.5" />
Delete Guide
</>
) : (
'Deleting...'
)}
</button>
)}
{isConfirming && (
<span className="flex w-full items-center justify-between gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70">
Are you sure?
<div className="flex items-center gap-2">
<button
onClick={() => {
setIsConfirming(false);
deleteCourse();
}}
disabled={isDeleting}
className="text-red-500 underline hover:text-red-800"
>
Yes
</button>
<button
onClick={() => setIsConfirming(false)}
className="text-red-500 underline hover:text-red-800"
>
No
</button>
</div>
</span>
)}
</div>
)}
</div>
);
}

View File

@@ -1,52 +0,0 @@
import type { ListUserAIGuidesResponse } from '../../queries/ai-guide';
import { AIGuideActions } from './AIGuideActions';
type AIGuideCardProps = {
guide: ListUserAIGuidesResponse['data'][number] & {
html: string;
};
showActions?: boolean;
};
export function AIGuideCard(props: AIGuideCardProps) {
const { guide, showActions = true } = props;
const guideDepthColor =
{
essentials: 'text-green-700',
detailed: 'text-blue-700',
complete: 'text-purple-700',
}[guide.depth] || 'text-gray-700';
return (
<div className="relative flex flex-grow flex-col">
<a
href={`/ai/guide/${guide.slug}`}
className="group relative flex h-full min-h-[120px] w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-3 text-left hover:border-gray-300 hover:bg-gray-50 sm:p-4"
>
<div className="mb-2 flex items-center justify-between sm:mb-3">
<span
className={`rounded-full text-xs font-medium capitalize opacity-80 ${guideDepthColor}`}
>
{guide.depth}
</span>
</div>
<div className="relative max-h-[180px] min-h-[140px] overflow-y-hidden sm:max-h-[200px] sm:min-h-[160px]">
<div
className="prose prose-sm prose-pre:bg-gray-100 [&_h1]:hidden [&_h1:first-child]:block [&_h1:first-child]:text-base [&_h1:first-child]:font-bold [&_h1:first-child]:leading-[1.35] [&_h1:first-child]:text-pretty sm:[&_h1:first-child]:text-lg [&_h2]:hidden [&_h3]:hidden [&_h4]:hidden [&_h5]:hidden [&_h6]:hidden"
dangerouslySetInnerHTML={{ __html: guide.html }}
/>
<div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-white to-transparent group-hover:from-gray-50 sm:h-16" />
</div>
</a>
{showActions && guide.slug && (
<div className="absolute top-2 right-2">
<AIGuideActions guideSlug={guide.slug} />
</div>
)}
</div>
);
}

View File

@@ -1,34 +0,0 @@
import { useState } from 'react';
import { AITutorHeader } from '../AITutor/AITutorHeader';
import { AITutorLayout } from '../AITutor/AITutorLayout';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { LibraryTabs } from '../Library/LibraryTab';
type AILibraryLayoutProps = {
activeTab: 'courses' | 'guides';
children: React.ReactNode;
};
export function AILibraryLayout(props: AILibraryLayoutProps) {
const { activeTab, children } = props;
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
return (
<AITutorLayout activeTab="library">
{showUpgradePopup && (
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<div className="mx-auto flex w-full max-w-6xl flex-grow flex-col p-2">
<AITutorHeader
title="Library"
subtitle="Explore your AI-generated guides and courses"
onUpgradeClick={() => setShowUpgradePopup(true)}
/>
<LibraryTabs activeTab={activeTab} />
{children}
</div>
</AITutorLayout>
);
}

View File

@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { AICourseCard } from '../GenerateCourse/AICourseCard';
import { AILoadingState } from './AILoadingState';
import { AITutorHeader } from './AITutorHeader';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import {
@@ -12,15 +13,14 @@ import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import { Pagination } from '../Pagination/Pagination';
import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
import { AITutorTallMessage } from './AITutorTallMessage';
import { BookOpen, Loader2 } from 'lucide-react';
import { humanizeNumber } from '../../lib/number';
import { BookOpen } from 'lucide-react';
export function AIExploreCourseListing() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const [pageState, setPageState] = useState<ListExploreAiCoursesQuery>({
perPage: '42',
perPage: '21',
currPage: '1',
query: '',
});
@@ -36,7 +36,6 @@ export function AIExploreCourseListing() {
}, [exploreAiCourses]);
const courses = exploreAiCourses?.data ?? [];
const isAnyLoading = isExploreAiCoursesLoading || isInitialLoading;
useEffect(() => {
const queryParams = getUrlParams();
@@ -64,91 +63,66 @@ export function AIExploreCourseListing() {
<AITutorHeader
title="Explore Courses"
subtitle="Explore the AI courses created by community"
onUpgradeClick={() => setShowUpgradePopup(true)}
/>
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
disabled={isAnyLoading}
/>
>
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</AITutorHeader>
{isAnyLoading && (
<p className="mb-4 flex flex-row items-center gap-2 text-sm text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading courses...
</p>
{(isInitialLoading || isExploreAiCoursesLoading) && (
<AILoadingState
title="Loading courses"
subtitle="This may take a moment..."
/>
)}
{!isAnyLoading && (
<>
<div className="mb-4 flex items-center justify-between">
<p className="text-sm text-gray-500">
Community has generated{' '}
{humanizeNumber(exploreAiCourses?.totalCount || 0)} courses
</p>
<div className="hidden lg:block">
<Pagination
variant="minimal"
totalCount={exploreAiCourses?.totalCount || 0}
totalPages={exploreAiCourses?.totalPages || 0}
currPage={Number(exploreAiCourses?.currPage || 1)}
perPage={Number(exploreAiCourses?.perPage || 21)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className=""
{!isExploreAiCoursesLoading && courses && courses.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<AICourseCard
key={course._id}
course={course}
showActions={false}
showProgress={false}
/>
</div>
))}
</div>
{courses && courses.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<AICourseCard
key={course._id}
course={course}
showActions={false}
showProgress={false}
variant="column"
/>
))}
</div>
<Pagination
totalCount={exploreAiCourses?.totalCount || 0}
totalPages={exploreAiCourses?.totalPages || 0}
currPage={Number(exploreAiCourses?.currPage || 1)}
perPage={Number(exploreAiCourses?.perPage || 21)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{courses.length === 0 && (
<AITutorTallMessage
title="No courses found"
subtitle="Try a different search or check back later."
icon={BookOpen}
buttonText="Create your first course"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
)}
</>
<Pagination
totalCount={exploreAiCourses?.totalCount || 0}
totalPages={exploreAiCourses?.totalPages || 0}
currPage={Number(exploreAiCourses?.currPage || 1)}
perPage={Number(exploreAiCourses?.perPage || 21)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{!isInitialLoading &&
!isExploreAiCoursesLoading &&
courses.length === 0 && (
<AITutorTallMessage
title="No courses found"
subtitle="Try a different search or check back later."
icon={BookOpen}
buttonText="Create your first course"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
)}
</>
);
}

View File

@@ -20,7 +20,7 @@ export function AIFeaturedCoursesListing() {
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const [pageState, setPageState] = useState<ListUserAiCoursesQuery>({
perPage: '42',
perPage: '21',
currPage: '1',
query: '',
});
@@ -63,8 +63,7 @@ export function AIFeaturedCoursesListing() {
)}
<AITutorHeader
title="Staff Picks"
subtitle="Explore our hand-picked courses generated by AI"
title="Featured Courses"
onUpgradeClick={() => setShowUpgradePopup(true)}
>
<AICourseSearch
@@ -97,7 +96,6 @@ export function AIFeaturedCoursesListing() {
course={course}
showActions={false}
showProgress={false}
variant="column"
/>
))}
</div>

View File

@@ -1,18 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { AITutorLimits } from './AITutorLimits';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useIsPaidUser } from '../../queries/billing';
import { PlusIcon } from 'lucide-react';
type AITutorHeaderProps = {
title: string;
subtitle?: string;
onUpgradeClick: () => void;
children?: React.ReactNode;
};
export function AITutorHeader(props: AITutorHeaderProps) {
const { title, subtitle, onUpgradeClick, children } = props;
const { title, onUpgradeClick, children } = props;
const { data: limits } = useQuery(getAiCourseLimitOptions(), queryClient);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
@@ -21,22 +20,20 @@ export function AITutorHeader(props: AITutorHeaderProps) {
return (
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex w-full flex-row items-center justify-between gap-2">
<div className="gap-2">
<h2 className="relative top-0 mb-1 sm:mb-3 flex-shrink-0 text-2xl sm:text-3xl font-semibold lg:top-1">
{title}
</h2>
{subtitle && <p className="mb-4 text-sm text-gray-500">{subtitle}</p>}
</div>
<div className="flex flex-row items-center gap-2">
<a
href="/ai"
className="flex max-sm:hidden flex-row items-center gap-2 rounded-lg bg-black px-4 py-1.5 text-sm font-medium text-white"
>
<PlusIcon className="h-4 w-4" />
New
</a>
</div>
<div className="flex items-center gap-2">
<h2 className="relative flex-shrink-0 top-0 lg:top-1 text-lg font-semibold">{title}</h2>
</div>
<div className="flex items-center gap-2">
<AITutorLimits
used={used}
limit={limit}
isPaidUser={isPaidUser}
isPaidUserLoading={isPaidUserLoading}
onUpgradeClick={onUpgradeClick}
/>
{children}
</div>
</div>
);

View File

@@ -6,13 +6,12 @@ import { cn } from '../../lib/classname';
type AITutorLayoutProps = {
children: React.ReactNode;
activeTab?: AITutorTab;
activeTab: AITutorTab;
wrapperClassName?: string;
containerClassName?: string;
};
export function AITutorLayout(props: AITutorLayoutProps) {
const { children, activeTab, wrapperClassName, containerClassName } = props;
const { children, activeTab, wrapperClassName } = props;
const [isSidebarFloating, setIsSidebarFloating] = useState(false);
@@ -30,12 +29,7 @@ export function AITutorLayout(props: AITutorLayoutProps) {
</button>
</div>
<div
className={cn(
'flex flex-grow flex-row lg:h-screen',
containerClassName,
)}
>
<div className="flex flex-grow flex-row lg:h-screen">
<AITutorSidebar
onClose={() => setIsSidebarFloating(false)}
isFloating={isSidebarFloating}

View File

@@ -1,33 +1,32 @@
import { BookOpen, Compass, Plus, Star, X, Zap } from 'lucide-react';
import {
BookOpen, Compass,
Plus,
Star,
X,
Zap
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { isLoggedIn } from '../../lib/jwt';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { queryClient } from '../../stores/query-client';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { getPercentage } from '../../lib/number';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
import { cn } from '../../lib/classname';
import { UserDropdown } from './UserDropdown';
type AITutorSidebarProps = {
isFloating: boolean;
activeTab?: AITutorTab;
activeTab: AITutorTab;
onClose: () => void;
};
const sidebarItems = [
{
key: 'new',
label: 'New',
label: 'New Course',
href: '/ai',
icon: Plus,
},
{
key: 'library',
label: 'Library',
key: 'courses',
label: 'My Courses',
href: '/ai/courses',
icon: BookOpen,
},
@@ -58,47 +57,25 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const totalPercentage = getPercentage(used, limit);
useEffect(() => {
setIsInitialLoad(false);
}, []);
const isLoading = isPaidUserLoading || isLimitsLoading;
return (
<>
{isUpgradeModalOpen && (
<UpgradeAccountModal onClose={() => setIsUpgradeModalOpen(false)} />
)}
{showAILimitsPopup && (
<AILimitsPopup
onClose={() => setShowAILimitsPopup(false)}
onUpgrade={() => {
setIsUpgradeModalOpen(true);
setShowAILimitsPopup(false);
}}
/>
)}
<aside
className={cn(
'flex w-[255px] shrink-0 flex-col border-r border-slate-200',
className={`w-[255px] shrink-0 border-r border-slate-200 ${
isFloating
? 'fixed top-0 bottom-0 left-0 z-50 flex border-r-0 bg-white shadow-xl'
: 'hidden lg:flex',
)}
? 'fixed top-0 bottom-0 left-0 z-50 block border-r-0 bg-white shadow-xl'
: 'hidden lg:block'
}`}
>
{isFloating && (
<button className="absolute top-3 right-3" onClick={onClose}>
@@ -128,52 +105,47 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
</p>
</div>
<ul className="list-none space-y-1">
<ul className="space-y-1">
{sidebarItems.map((item) => (
<li key={item.key}>
<AITutorSidebarItem
item={item}
isActive={activeTab === item.key}
/>
<a
href={item.href}
className={`font-regular flex w-full items-center border-r-2 px-5 py-2 text-sm transition-all ${
activeTab === item.key
? 'border-r-black bg-gray-100 text-black'
: 'border-r-transparent text-gray-500 hover:border-r-gray-300'
}`}
>
<span className="flex grow items-center">
<item.icon className="mr-2 size-4" />
{item.label}
</span>
</a>
</li>
))}
{!isInitialLoad && isLoggedIn() && !isPaidUser && !isLoading && (
<li>
<button
onClick={() => {
setIsUpgradeModalOpen(true);
}}
className="animate-fade-in mx-4 mt-4 rounded-xl bg-amber-100 p-4 text-left transition-colors hover:bg-amber-200/80"
>
<span className="mb-2 flex items-center gap-2">
<Zap className="size-4 text-amber-600" />
<span className="font-medium text-amber-900">Upgrade</span>
</span>
<span className="mt-1 block text-left text-xs leading-4 text-amber-700">
Get access to all features and benefits of the AI Tutor.
</span>
<div className="mt-5">
<div className="relative h-1 w-full rounded-full bg-amber-300/40">
<div
className="absolute inset-0 h-full rounded-full bg-amber-600/80"
style={{
width: `${totalPercentage}%`,
}}
></div>
</div>
<span className="mt-2 block text-xs text-amber-700">
{totalPercentage}% of the daily limit used
{!isInitialLoad &&
isLoggedIn() &&
!isPaidUser &&
!isPaidUserLoading && (
<li>
<button
onClick={() => {
setIsUpgradeModalOpen(true);
}}
className="mx-4 mt-4 rounded-xl bg-amber-100 p-4 text-left transition-colors hover:bg-amber-200/80"
>
<span className="mb-2 flex items-center gap-2">
<Zap className="size-4 text-amber-600" />
<span className="font-medium text-amber-900">Upgrade</span>
</span>
</div>
</button>
</li>
)}
<span className="mt-1 block text-left text-xs leading-4 text-amber-700">
Get access to all features and benefits of the AI Tutor.
</span>
</button>
</li>
)}
</ul>
<div className="mx-2 mt-auto mb-2">
<UserDropdown />
</div>
</aside>
{isFloating && (
<div className="fixed inset-0 z-40 bg-black/50" onClick={onClose} />
@@ -181,36 +153,3 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
</>
);
}
type AITutorSidebarItemProps = {
item: (typeof sidebarItems)[number];
as?: 'a' | 'button';
onClick?: () => void;
className?: string;
isActive?: boolean;
};
function AITutorSidebarItem(props: AITutorSidebarItemProps) {
const { item, as = 'a', onClick, className, isActive } = props;
const Component = as;
return (
<Component
{...(as === 'a' ? { href: item.href } : {})}
{...(as === 'button' ? { onClick } : {})}
className={cn(
'font-regular flex w-full items-center border-r-2 px-5 py-2 text-sm transition-all',
isActive
? 'border-r-black bg-gray-100 text-black'
: 'border-r-transparent text-gray-500 hover:border-r-gray-300',
className,
)}
>
<span className="flex grow items-center">
<item.icon className="mr-2 size-4" />
{item.label}
</span>
</Component>
);
}

View File

@@ -1,75 +0,0 @@
import { ChevronDown } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
import { cn } from '../../lib/classname';
import type { LucideIcon } from 'lucide-react';
type BaseDropdownProps<T extends string> = {
value: T;
options: readonly T[];
onChange: (value: T) => void;
icons?: Record<T, LucideIcon>;
};
export function BaseDropdown<T extends string>(props: BaseDropdownProps<T>) {
const { value, options, onChange, icons } = props;
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const Icon = icons?.[value];
return (
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className={cn(
'flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200 hover:text-black',
)}
>
{Icon && <Icon size={16} />}
<span className="capitalize">{value}</span>
<ChevronDown size={16} className={cn(isOpen && 'rotate-180')} />
</button>
{isOpen && (
<div className="absolute z-10 mt-1 flex flex-col overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
{options.map((option) => {
const OptionIcon = icons?.[option];
return (
<button
key={option}
type="button"
onClick={() => {
onChange(option);
setIsOpen(false);
}}
className={cn(
'flex items-center gap-2 px-5 py-2 text-left text-sm capitalize hover:bg-gray-100',
value === option && 'bg-gray-200 font-medium hover:bg-gray-200',
)}
>
{OptionIcon && <OptionIcon size={16} />}
{option}
</button>
);
})}
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,9 @@
import { BaseDropdown } from './BaseDropdown';
import { ChevronDown } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
import { cn } from '../../lib/classname';
import {
difficultyLevels,
type DifficultyLevel,
difficultyLevels,
type DifficultyLevel,
} from '../GenerateCourse/AICourse';
type DifficultyDropdownProps = {
@@ -12,11 +14,56 @@ type DifficultyDropdownProps = {
export function DifficultyDropdown(props: DifficultyDropdownProps) {
const { value, onChange } = props;
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<BaseDropdown
value={value}
options={difficultyLevels}
onChange={onChange}
/>
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className={cn(
'flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200 hover:text-black',
)}
>
<span className="capitalize">{value}</span>
<ChevronDown size={16} className={cn(isOpen && 'rotate-180')} />
</button>
{isOpen && (
<div className="absolute z-10 mt-1 flex flex-col overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
{difficultyLevels.map((level) => (
<button
key={level}
type="button"
onClick={() => {
onChange(level);
setIsOpen(false);
}}
className={cn(
'px-5 py-2 text-left text-sm capitalize hover:bg-gray-100',
value === level && 'bg-gray-200 font-medium hover:bg-gray-200',
)}
>
{level}
</button>
))}
</div>
)}
</div>
);
}

View File

@@ -1,28 +0,0 @@
import { BaseDropdown } from './BaseDropdown';
import { BookOpen, FileText } from 'lucide-react';
export const natureTypes = ['course', 'document'] as const;
export type NatureType = (typeof natureTypes)[number];
const natureIcons = {
course: BookOpen,
document: FileText,
} as const;
type NatureDropdownProps = {
value: NatureType;
onChange: (value: NatureType) => void;
};
export function NatureDropdown(props: NatureDropdownProps) {
const { value, onChange } = props;
return (
<BaseDropdown
value={value}
options={natureTypes}
onChange={onChange}
icons={natureIcons}
/>
);
}

View File

@@ -1,121 +0,0 @@
import {
ChevronDown,
CreditCardIcon,
LogInIcon,
LogOutIcon,
Settings,
User2,
} from 'lucide-react';
import { useAuth } from '../../hooks/use-auth';
import { useClientMount } from '../../hooks/use-client-mount';
import { logout } from '../../lib/auth';
import { showLoginPopup } from '../../lib/popup';
import { useIsPaidUser } from '../../queries/billing';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../DropdownMenu';
type UserDropdownProps = {};
export function UserDropdown(props: UserDropdownProps) {
const currentUser = useAuth();
const { isPaidUser, isLoading } = useIsPaidUser();
const isMounted = useClientMount();
if (!isMounted || isLoading) {
return null;
}
if (!currentUser) {
return (
<button
onClick={showLoginPopup}
className="animate-fade-in inline-flex h-auto w-full items-center justify-center gap-2 rounded-lg border border-gray-700 bg-black px-4 py-2.5 text-sm font-medium text-white transition-all duration-200 outline-none hover:!opacity-80 disabled:cursor-not-allowed disabled:opacity-60"
>
<LogInIcon className="size-4" />
Free Signup or Login
</button>
);
}
const userAvatar = currentUser?.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser?.avatar}`
: '/images/default-avatar.png';
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="group flex w-full items-center gap-3 rounded-lg border border-transparent px-4 py-2.5 text-sm font-medium transition-colors hover:bg-gray-100 hover:text-black focus:outline-none data-[state=open]:bg-gray-100 data-[state=open]:text-black">
<div className="relative size-7 shrink-0 overflow-hidden rounded-full">
<img
src={userAvatar}
alt={currentUser.name}
className="absolute inset-0 h-full w-full object-cover"
/>
</div>
<div className="flex min-w-0 flex-1 flex-col text-left">
<span className="truncate font-medium text-gray-900">
{currentUser.name}
</span>
<span className="truncate text-xs text-gray-500">
{isPaidUser ? 'Pro Member' : 'Free User'}
</span>
</div>
<ChevronDown className="size-4 text-gray-400 transition-transform duration-200 group-data-[state=open]:rotate-180" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[var(--radix-dropdown-menu-trigger-width)] min-w-52 rounded-lg border border-gray-200 bg-white p-1">
<div className="space-y-1">
<DropdownMenuItem asChild>
<a
href="/account"
className="flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-black"
>
<User2 className="size-4" />
Account
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
href="/account/billing"
className="flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-black"
>
<CreditCardIcon className="size-4" />
Billing
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
href="/account/settings"
className="flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-black"
>
<Settings className="size-4" />
Settings
</a>
</DropdownMenuItem>
</div>
<DropdownMenuSeparator className="my-1" />
<DropdownMenuItem
className="flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-700"
onSelect={() => {
logout();
}}
>
<LogOutIcon className="size-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,10 +0,0 @@
<!-- OneTrust Cookies Consent Notice start for roadmap.sh -->
<script
src='https://cdn.cookielaw.org/scripttemplates/otSDKStub.js'
type='text/javascript'
charset='UTF-8'
data-domain-script='01977e0e-9a37-7b4a-aad2-8cf9247d94b6'></script>
<script type='text/javascript'>
function OptanonWrapper() {}
</script>
<!-- OneTrust Cookies Consent Notice end for roadmap.sh -->

View File

@@ -23,20 +23,15 @@ declare global {
window.fireEvent = (props) => {
const { action, category, label, value, callback } = props;
const eventId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
if (['course', 'ai_tutor'].includes(category)) {
const url = new URL(import.meta.env.PUBLIC_API_URL);
url.pathname = '/api/_t';
url.pathname = '/_t';
url.searchParams.set('action', action);
url.searchParams.set('category', category);
url.searchParams.set('label', label ?? '');
url.searchParams.set('value', value ?? '');
url.searchParams.set('event_id', eventId);
httpPost(url.toString(), {}).catch(console.error);
return;
}
if (!window.gtag) {
@@ -54,8 +49,6 @@ window.fireEvent = (props) => {
event_category: category,
event_label: label,
value: value,
event_id: eventId,
source: 'client',
...(callback ? { event_callback: callback } : {}),
});
};

View File

@@ -1,6 +1,7 @@
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { REDIRECT_PAGE_AFTER_AUTH } from '../../lib/auth';
export const REDIRECT_PAGE_AFTER_AUTH = 'redirect_page_after_auth';
function easeInElement(el: Element) {
el.classList.add('opacity-0', 'transition-opacity', 'duration-300');

View File

@@ -41,12 +41,13 @@ export function BillingPage() {
queryClient,
);
const willBeCanceled = billingDetails?.cancelAtPeriodEnd;
const isCanceled = billingDetails?.status === 'canceled';
const isCanceled =
billingDetails?.status === 'canceled' ||
billingDetails?.status === 'incomplete_expired' ||
billingDetails?.cancelAtPeriodEnd;
const isPastDue = billingDetails?.status === 'past_due';
const isIncomplete = billingDetails?.status === 'incomplete';
const isIncompleteExpired = billingDetails?.status === 'incomplete_expired';
const {
mutate: createCustomerPortal,
@@ -100,7 +101,7 @@ export function BillingPage() {
day: 'numeric',
});
const modals = (
return (
<>
{showUpgradeModal && (
<UpgradeAccountModal
@@ -111,190 +112,140 @@ export function BillingPage() {
)}
{showVerifyUpgradeModal && <VerifyUpgrade />}
</>
);
if (billingDetails?.status === 'none' || isIncompleteExpired) {
return (
<>
{modals}
{billingDetails?.status === 'none' && !isLoadingBillingDetails && (
<EmptyBillingScreen onUpgrade={() => setShowUpgradeModal(true)} />
</>
);
}
)}
if (isCanceled) {
return (
<>
{modals}
<BillingWarning
icon={CircleX}
message="Your subscription has been canceled."
buttonText="Reactivate?"
onButtonClick={() => {
if (willBeCanceled) {
createCustomerPortal({});
} else {
setShowUpgradeModal(true);
}
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
<EmptyBillingScreen onUpgrade={() => setShowUpgradeModal(true)} />
</>
);
}
if (isIncomplete) {
return (
<>
{modals}
<BillingWarning
icon={AlertCircle}
message="Your subscription is incomplete "
buttonText="please pay invoice on Stripe."
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
<EmptyBillingScreen onUpgrade={() => setShowUpgradeModal(true)} />
</>
);
}
if (!priceDetails) {
return (
<div className="p-5">
<h1 className="text-2xl font-bold">Uh oh!</h1>
<p className="text-sm text-gray-500">
We couldn't find your subscription details. Please contact support at
<a className="text-blue-500 underline" href="mailto:info@roadmap.sh">
info@roadmap.sh
</a>
.
</p>
</div>
);
}
return (
<>
{modals}
<div className="mt-1">
{isPastDue && (
<BillingWarning
message="We were not able to charge your card."
buttonText="Update payment information."
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
{willBeCanceled && (
<BillingWarning
icon={CircleX}
message={`Your subscription will be canceled on ${formattedNextBillDate}. `}
buttonText="Reactivate?"
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
<h2 className="mb-2 text-xl font-semibold text-black">
Current Subscription
</h2>
<p className="text-sm text-gray-500">
Thank you for being a pro member. Your plan details are below.
</p>
<div className="mt-8 flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<RefreshCw className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs tracking-wider text-gray-400 uppercase">
Payment
</span>
<h3 className="flex items-baseline text-lg font-semibold text-black">
${priceDetails.amount}
<span className="ml-1 text-sm font-normal text-gray-500">
/ {priceDetails.interval}
</span>
</h3>
</div>
</div>
</div>
<div
className={cn(
'mt-6 pt-6',
!isIncomplete && 'border-t border-gray-100',
isIncomplete && '-mt-6',
)}
>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<Calendar className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs tracking-wider text-gray-400 uppercase">
{willBeCanceled ? 'Expires On' : 'Renews On'}
</span>
<h3 className="text-lg font-semibold text-black">
{formattedNextBillDate}
</h3>
</div>
</div>
<div className="mt-8 flex gap-3 max-sm:flex-col">
{!willBeCanceled && (
<button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-xs transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-black focus:ring-offset-2 focus:outline-hidden max-sm:grow"
onClick={() => {
setShowUpgradeModal(true);
{billingDetails?.status !== 'none' &&
!isLoadingBillingDetails &&
priceDetails && (
<div className="mt-1">
{isIncomplete && (
<BillingWarning
icon={AlertCircle}
message="Your subscription is incomplete "
buttonText="please pay invoice on Stripe."
onButtonClick={() => {
createCustomerPortal({});
}}
>
<ArrowRightLeft className="mr-2 h-4 w-4" />
Switch Plan
</button>
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
<button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-xs transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-black focus:ring-offset-2 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => {
createCustomerPortal({});
}}
disabled={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
>
{isCreatingCustomerPortal || isCreatingCustomerPortalSuccess ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<CreditCard className="mr-2 h-4 w-4" />
{isCanceled && (
<BillingWarning
icon={CircleX}
message="Your subscription has been canceled."
buttonText="Reactivate?"
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
{isPastDue && (
<BillingWarning
message="We were not able to charge your card."
buttonText="Update payment information."
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
<h2 className="mb-2 text-xl font-semibold text-black">
Current Subscription
</h2>
<p className="text-sm text-gray-500">
Thank you for being a pro member. Your plan details are below.
</p>
<div className="mt-8 flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<RefreshCw className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs tracking-wider text-gray-400 uppercase">
Payment
</span>
<h3 className="flex items-baseline text-lg font-semibold text-black">
${priceDetails.amount}
<span className="ml-1 text-sm font-normal text-gray-500">
/ {priceDetails.interval}
</span>
</h3>
</div>
</div>
</div>
<div
className={cn(
'mt-6 pt-6',
!isIncomplete && 'border-t border-gray-100',
isIncomplete && '-mt-6',
)}
Manage Subscription
</button>
>
{!isIncomplete && (
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<Calendar className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs tracking-wider text-gray-400 uppercase">
{billingDetails?.cancelAtPeriodEnd
? 'Expires On'
: 'Renews On'}
</span>
<h3 className="text-lg font-semibold text-black">
{formattedNextBillDate}
</h3>
</div>
</div>
)}
<div className="mt-8 flex gap-3 max-sm:flex-col">
{!isCanceled && !isIncomplete && (
<button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-xs transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-black focus:ring-offset-2 focus:outline-hidden max-sm:grow"
onClick={() => {
setShowUpgradeModal(true);
}}
>
<ArrowRightLeft className="mr-2 h-4 w-4" />
Switch Plan
</button>
)}
<button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-xs transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-black focus:ring-offset-2 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => {
createCustomerPortal({});
}}
disabled={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
>
{isCreatingCustomerPortal ||
isCreatingCustomerPortalSuccess ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<CreditCard className="mr-2 h-4 w-4" />
)}
Manage Subscription
</button>
</div>
</div>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -120,11 +120,10 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
queryClient,
);
const isCanceled = userBillingDetails?.status === 'canceled';
const selectedPlanDetails = USER_SUBSCRIPTION_PLAN_PRICES.find(
(plan) => plan.interval === selectedPlan,
);
const currentPlanPriceId = isCanceled ? null : userBillingDetails?.priceId;
const currentPlanPriceId = userBillingDetails?.priceId;
const currentPlan = USER_SUBSCRIPTION_PLAN_PRICES.find(
(plan) => plan.priceId === currentPlanPriceId,
);
@@ -185,7 +184,6 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
bodyClassName="p-4 sm:p-6 bg-white"
wrapperClassName="h-auto rounded-xl max-w-3xl w-full min-h-[540px] mx-2 sm:mx-4"
overlayClassName="items-start md:items-center"
hasCloseButton={true}
>
<div onClick={(e) => e.stopPropagation()}>
{errorContent}

View File

@@ -1,229 +0,0 @@
import {
BookOpenIcon,
FileTextIcon,
SparklesIcon,
type LucideIcon,
} from 'lucide-react';
import { useEffect, useId, useState, type FormEvent } from 'react';
import { FormatItem } from './FormatItem';
import { GuideOptions } from './GuideOptions';
import { FineTuneCourse } from '../GenerateCourse/FineTuneCourse';
import { CourseOptions } from './CourseOptions';
import {
clearFineTuneData,
getCourseFineTuneData,
getLastSessionId,
storeFineTuneData,
} from '../../lib/ai';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { useIsPaidUser } from '../../queries/billing';
import { cn } from '../../lib/classname';
const allowedFormats = ['course', 'guide', 'roadmap'] as const;
type AllowedFormat = (typeof allowedFormats)[number];
export function ContentGenerator() {
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const [title, setTitle] = useState('');
const [selectedFormat, setSelectedFormat] = useState<AllowedFormat>('course');
// guide options
const [depth, setDepth] = useState('essentials');
// course options
const [difficulty, setDifficulty] = useState('beginner');
// fine-tune options
const [showFineTuneOptions, setShowFineTuneOptions] = useState(false);
const [about, setAbout] = useState('');
const [goal, setGoal] = useState('');
const [customInstructions, setCustomInstructions] = useState('');
const titleFieldId = useId();
const fineTuneOptionsId = useId();
const allowedFormats: {
label: string;
icon: LucideIcon;
value: AllowedFormat;
}[] = [
{
label: 'Course',
icon: BookOpenIcon,
value: 'course',
},
{
label: 'Guide',
icon: FileTextIcon,
value: 'guide',
},
];
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!isLoggedIn()) {
showLoginPopup();
return;
}
let sessionId = '';
if (showFineTuneOptions) {
clearFineTuneData();
sessionId = storeFineTuneData({
about,
goal,
customInstructions,
});
}
if (selectedFormat === 'course') {
window.location.href = `/ai/course?term=${encodeURIComponent(title)}&difficulty=${difficulty}&id=${sessionId}&format=${selectedFormat}`;
} else if (selectedFormat === 'guide') {
window.location.href = `/ai/guide?term=${encodeURIComponent(title)}&depth=${depth}&id=${sessionId}&format=${selectedFormat}`;
}
};
useEffect(() => {
window?.fireEvent({
action: 'tutor_user',
category: 'ai_tutor',
label: 'Visited AI Course Page',
});
}, []);
useEffect(() => {
const lastSessionId = getLastSessionId();
if (!lastSessionId) {
return;
}
const fineTuneData = getCourseFineTuneData(lastSessionId);
if (!fineTuneData) {
return;
}
setAbout(fineTuneData.about);
setGoal(fineTuneData.goal);
setCustomInstructions(fineTuneData.customInstructions);
}, []);
return (
<div className="mx-auto flex w-full max-w-2xl flex-grow flex-col pt-4 md:justify-center md:pt-10 lg:pt-4">
<div className="relative">
{isUpgradeModalOpen && (
<UpgradeAccountModal onClose={() => setIsUpgradeModalOpen(false)} />
)}
{!isPaidUser && !isPaidUserLoading && isLoggedIn() && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 -translate-y-8 text-gray-500 max-md:hidden">
You are on the free plan
<button
onClick={() => setIsUpgradeModalOpen(true)}
className="ml-2 rounded-xl bg-yellow-600 px-2 py-1 text-sm text-white hover:opacity-80"
>
Upgrade to Pro
</button>
</div>
)}
<h1 className="mb-0.5 text-center text-4xl font-semibold max-md:text-left max-md:text-xl lg:mb-3">
What can I help you learn?
</h1>
<p className="text-center text-lg text-balance text-gray-600 max-md:text-left max-md:text-sm">
Enter a topic below to generate a personalized course for it
</p>
</div>
<form className="mt-10 space-y-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<label htmlFor={titleFieldId} className="inline-block text-gray-500">
What can I help you learn?
</label>
<input
type="text"
id={titleFieldId}
placeholder="Enter a topic"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="block w-full rounded-xl border border-gray-200 bg-white p-4 outline-none placeholder:text-gray-500 focus:border-gray-500"
required
minLength={3}
/>
</div>
<div className="flex flex-col gap-2">
<label className="inline-block text-gray-500">
Choose the format
</label>
<div className="grid grid-cols-2 gap-3">
{allowedFormats.map((format) => {
const isSelected = format.value === selectedFormat;
return (
<FormatItem
key={format.value}
label={format.label}
onClick={() => setSelectedFormat(format.value)}
icon={format.icon}
isSelected={isSelected}
/>
);
})}
</div>
</div>
{selectedFormat === 'guide' && (
<GuideOptions depth={depth} setDepth={setDepth} />
)}
{selectedFormat === 'course' && (
<CourseOptions
difficulty={difficulty}
setDifficulty={setDifficulty}
/>
)}
{selectedFormat !== 'roadmap' && (
<>
<label
className={cn(
'flex items-center gap-2 border border-gray-200 bg-white p-4',
showFineTuneOptions && 'rounded-t-xl',
!showFineTuneOptions && 'rounded-xl',
)}
htmlFor={fineTuneOptionsId}
>
<input
type="checkbox"
id={fineTuneOptionsId}
checked={showFineTuneOptions}
onChange={(e) => setShowFineTuneOptions(e.target.checked)}
/>
Explain more for a better result
</label>
{showFineTuneOptions && (
<FineTuneCourse
hasFineTuneData={showFineTuneOptions}
about={about}
goal={goal}
customInstructions={customInstructions}
setAbout={setAbout}
setGoal={setGoal}
setCustomInstructions={setCustomInstructions}
className="-mt-4.5 overflow-hidden rounded-b-xl border border-gray-200 bg-white [&_div:first-child_label]:border-t-0"
/>
)}
</>
)}
<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-black p-4 text-white focus:outline-none"
>
<SparklesIcon className="size-4" />
Generate
</button>
</form>
</div>
);
}

View File

@@ -1,79 +0,0 @@
import { useId, useState } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../Select';
type CourseOptionsProps = {
difficulty: string;
setDifficulty: (difficulty: string) => void;
};
export function CourseOptions(props: CourseOptionsProps) {
const { difficulty, setDifficulty } = props;
const difficultySelectId = useId();
const difficultyOptions = [
{
label: 'Beginner',
value: 'beginner',
description: 'Covers fundamental concepts',
},
{
label: 'Intermediate',
value: 'intermediate',
description: 'Explore advanced topics',
},
{
label: 'Advanced',
value: 'advanced',
description: 'Deep dives into complex concepts',
},
];
const selectedDifficulty = difficultyOptions.find(
(option) => option.value === difficulty,
);
return (
<div className="flex flex-col gap-2">
<label
htmlFor={difficultySelectId}
className="inline-block text-gray-500"
>
Choose difficulty level
</label>
<Select value={difficulty} onValueChange={setDifficulty}>
<SelectTrigger
id={difficultySelectId}
className="h-auto rounded-xl bg-white p-4 text-base"
>
{selectedDifficulty && (
<div className="flex flex-col gap-1">
<span>{selectedDifficulty.label}</span>
</div>
)}
{!selectedDifficulty && (
<SelectValue placeholder="Select a difficulty" />
)}
</SelectTrigger>
<SelectContent className="rounded-xl bg-white">
{difficultyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col gap-1">
<span>{option.label}</span>
<span className="text-xs text-gray-500">
{option.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}

View File

@@ -1,29 +0,0 @@
import { type LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
type FormatItemProps = {
label: string;
onClick: () => void;
icon: LucideIcon;
isSelected: boolean;
};
export function FormatItem(props: FormatItemProps) {
const { label, onClick, icon: Icon, isSelected } = props;
return (
<button
type="button"
className={cn(
'flex w-full flex-col items-center justify-center gap-2.5 rounded-xl border border-gray-200 p-2 py-8',
isSelected
? 'border-gray-400 font-medium bg-white'
: 'bg-white text-gray-400 hover:bg-white hover:border-gray-300',
)}
onClick={onClick}
>
<Icon className="size-6" />
<span>{label}</span>
</button>
);
}

View File

@@ -1,72 +0,0 @@
import { useId } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../Select';
type GuideOptionsProps = {
depth: string;
setDepth: (depth: string) => void;
};
export function GuideOptions(props: GuideOptionsProps) {
const { depth, setDepth } = props;
const depthSelectId = useId();
const depthOptions = [
{
label: 'Essentials',
value: 'essentials',
description: 'Just the core concepts',
},
{
label: 'Detailed',
value: 'detailed',
description: 'In-depth explanation',
},
{
label: 'Complete',
value: 'complete',
description: 'Cover the topic fully',
},
];
const selectedDepth = depthOptions.find((option) => option.value === depth);
return (
<div className="flex flex-col gap-2">
<label htmlFor={depthSelectId} className="inline-block text-gray-500">
Choose depth of content
</label>
<Select value={depth} onValueChange={setDepth}>
<SelectTrigger
id={depthSelectId}
className="h-auto rounded-xl bg-white p-4 text-base"
>
{selectedDepth && (
<div className="flex flex-col gap-1">
<span>{selectedDepth.label}</span>
</div>
)}
{!selectedDepth && <SelectValue placeholder="Select a depth" />}
</SelectTrigger>
<SelectContent className="rounded-xl bg-white">
{depthOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col gap-1">
<span>{option.label}</span>
<span className="text-xs text-gray-500">
{option.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { cn } from '../lib/classname';
import { Cookie } from 'lucide-react';
export function CookieSettingsButton() {
return (
<div className="mt-12 flex items-center justify-start">
<button
onClick={() => {
// @ts-ignore
const ot: any = window.OneTrust;
// @ts-ignore
const optanon: any = window.Optanon;
if (ot) {
ot.ToggleInfoDisplay();
} else if (optanon) {
optanon.ToggleInfoDisplay();
} else {
console.warn('OneTrust/Optanon SDK not found or not loaded yet.');
}
}}
className={cn(
'flex items-center gap-2 rounded-md bg-slate-800/80 px-3 py-1.5 text-sm text-gray-400 transition-colors hover:bg-slate-700 hover:text-white',
)}
>
<Cookie className="h-4 w-4" />
Cookie Settings
</button>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { type FormEvent, useEffect, useState } from 'react';
import { httpDelete } from '../../lib/http';
import { logout } from '../../lib/auth';
import { logout } from '../Navigation/navigation';
export function DeleteAccountForm() {
const [isLoading, setIsLoading] = useState(false);
@@ -24,7 +24,7 @@ export function DeleteAccountForm() {
}
const { response, error } = await httpDelete(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-account`,
`${import.meta.env.PUBLIC_API_URL}/v1-delete-account`
);
if (error || !response) {

View File

@@ -1,197 +0,0 @@
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '../lib/classname';
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:bg-gray-100 data-[state=open]:bg-gray-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-45 overflow-hidden rounded-lg border border-gray-200 bg-white p-0.5 text-black shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm text-black outline-none select-none focus:bg-gray-100 focus:text-black data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-0.5 h-px bg-gray-200', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};

View File

@@ -9,8 +9,8 @@ import {
type ResourceType,
} from '../../lib/resource-progress';
import { httpGet } from '../../lib/http';
import { ProgressNudge } from '../FrameRenderer/ProgressNudge';
import { getUrlParams } from '../../lib/browser.ts';
import { RoadmapFloatingChat } from '../FrameRenderer/RoadmapFloatingChat.tsx';
type EditorRoadmapProps = {
resourceId: string;
@@ -99,7 +99,7 @@ export function EditorRoadmap(props: EditorRoadmapProps) {
dimensions={dimensions}
resourceId={resourceId}
/>
<RoadmapFloatingChat roadmapId={resourceId} />
<ProgressNudge resourceId={resourceId} resourceType={resourceType} />
</div>
);
}

View File

@@ -1,7 +1,6 @@
---
import AstroIcon from './AstroIcon.astro';
import Icon from './AstroIcon.astro';
import { CookieSettingsButton } from './CookieSettingsButton';
---
<div class='bg-slate-900 py-6 pb-10 text-white sm:py-16'>
@@ -36,7 +35,7 @@ import { CookieSettingsButton } from './CookieSettingsButton';
>
</p>
<div class='flex flex-col justify-between gap-8 lg:flex-row lg:gap-2'>
<div class='flex flex-col justify-between gap-8 lg:gap-2 lg:flex-row'>
<div class='max-w-[425px]'>
<p class='text-md flex items-center'>
<a
@@ -57,9 +56,8 @@ import { CookieSettingsButton } from './CookieSettingsButton';
</a>
</p>
<p class='my-4 text-slate-300/60'>
Community created roadmaps, best practices, projects, articles,
resources and journeys to help you choose your path and grow in your
career.
Community created roadmaps, best practices, projects, articles, resources and journeys to help
you choose your path and grow in your career.
</p>
<div class='text-sm text-gray-400'>
<p>
@@ -75,10 +73,7 @@ import { CookieSettingsButton } from './CookieSettingsButton';
class='hover:text-white'
target='_blank'
>
<AstroIcon
icon='linkedin-2'
class='inline-block h-5 w-5 fill-current'
/>
<AstroIcon icon='linkedin-2' class='inline-block h-5 w-5 fill-current' />
</a>
<a
aria-label='Subscribe to YouTube channel'
@@ -119,15 +114,14 @@ import { CookieSettingsButton } from './CookieSettingsButton';
<img
src='/images/tns-sm.png'
alt='ThewNewStack'
class='my-1.5 mr-auto lg:mr-0 lg:ml-auto'
class='my-1.5 mr-auto lg:ml-auto lg:mr-0'
width='200'
height='24.8'
loading='lazy'
loading="lazy"
/>
</a>
<p class='my-4 text-slate-300/60'>
The top DevOps resource for Kubernetes, cloud-native computing, and
large-scale development and deployment.
The top DevOps resource for Kubernetes, cloud-native computing, and large-scale development and deployment.
</p>
<div class='text-sm text-gray-400'>
<p>
@@ -152,7 +146,5 @@ import { CookieSettingsButton } from './CookieSettingsButton';
</div>
</div>
</div>
<CookieSettingsButton client:load />
</div>
</div>

View File

@@ -4,7 +4,7 @@ svg text tspan {
text-rendering: optimizeSpeed;
}
code:not(pre code) {
code {
background: #1e1e3f;
color: #9efeff;
padding: 3px 5px;

View File

@@ -1,670 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import type { JSONContent } from '@tiptap/core';
import {
BookOpen,
ChevronDown,
Loader2Icon,
MessageCirclePlus,
PauseCircleIcon,
PersonStanding,
Plus,
SendIcon,
SquareArrowOutUpRight,
Trash2,
Wand2,
X,
} from 'lucide-react';
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { useKeydown } from '../../hooks/use-keydown';
import {
roadmapAIChatRenderer,
useRoadmapAIChat,
} from '../../hooks/use-roadmap-ai-chat';
import { cn } from '../../lib/classname';
import { lockBodyScroll } from '../../lib/dom';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { slugify } from '../../lib/slugger';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { chatHistoryOptions } from '../../queries/chat-history';
import { roadmapJSONOptions } from '../../queries/roadmap';
import { roadmapQuestionsOptions } from '../../queries/roadmap-questions';
import { queryClient } from '../../stores/query-client';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { RoadmapAIChatCard } from '../RoadmapAIChat/RoadmapAIChatCard';
import { RoadmapAIChatHistory } from '../RoadmapAIChatHistory/RoadmapAIChatHistory';
import { CLOSE_TOPIC_DETAIL_EVENT } from '../TopicDetail/TopicDetail';
import { UpdatePersonaModal } from '../UserPersona/UpdatePersonaModal';
type ChatHeaderButtonProps = {
onClick?: () => void;
href?: string;
icon: React.ReactNode;
children?: React.ReactNode;
className?: string;
target?: string;
};
export function ChatHeaderButton(props: ChatHeaderButtonProps) {
const { onClick, href, icon, children, className, target } = props;
const classNames = cn(
'flex shrink-0 items-center gap-1.5 text-xs text-gray-600 transition-colors hover:text-gray-900 min-w-8',
className,
);
if (!onClick && !href) {
return (
<span className={classNames}>
{icon}
{children && <span className="hidden sm:block">{children}</span>}
</span>
);
}
if (href) {
return (
<a
href={href}
target={target}
rel="noopener noreferrer"
className={classNames}
>
{icon}
{children && <span className="hidden sm:block">{children}</span>}
</a>
);
}
return (
<button onClick={onClick} className={classNames}>
{icon}
{children && <span className="hidden sm:block">{children}</span>}
</button>
);
}
type UpgradeMessageProps = {
onUpgradeClick?: () => void;
};
function UpgradeMessage(props: UpgradeMessageProps) {
const { onUpgradeClick } = props;
return (
<div className="border-t border-gray-200 bg-black px-3 py-3">
<div className="flex items-center gap-2.5">
<Wand2 className="h-4 w-4 flex-shrink-0 text-white" />
<div className="flex-1 text-sm">
<p className="mb-1 font-medium text-white">
You've reached your AI usage limit
</p>
<p className="text-xs text-gray-300">
Upgrade to Pro for relaxed limits and advanced features
</p>
</div>
<button
className="flex-shrink-0 rounded-md bg-white px-3 py-1.5 text-xs font-medium text-black transition-colors hover:bg-gray-100"
onClick={onUpgradeClick}
>
Upgrade to Pro
</button>
</div>
</div>
);
}
type UsageButtonProps = {
percentageUsed: number;
onUpgradeClick?: () => void;
};
function UsageButton(props: UsageButtonProps) {
const { percentageUsed, onUpgradeClick } = props;
return (
<button
onClick={onUpgradeClick}
className="flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium transition-all hover:bg-yellow-200"
>
<div className="hidden items-center gap-1.5 sm:flex">
<div className="h-1.5 w-6 overflow-hidden rounded-full bg-gray-200">
<div
className={cn(
'h-full transition-all duration-300',
percentageUsed >= 90
? 'bg-red-500'
: percentageUsed >= 70
? 'bg-yellow-500'
: 'bg-green-500',
)}
style={{ width: `${Math.min(percentageUsed, 100)}%` }}
/>
</div>
<span className="text-yellow-700">{percentageUsed}% used</span>
</div>
<span className="font-semibold text-yellow-800 underline underline-offset-2">
Upgrade
</span>
</button>
);
}
type RoadmapChatProps = {
roadmapId: string;
};
export function RoadmapFloatingChat(props: RoadmapChatProps) {
const { roadmapId } = props;
const [isOpen, setIsOpen] = useState(false);
const scrollareaRef = useRef<HTMLDivElement>(null);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const [isPersonalizeOpen, setIsPersonalizeOpen] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
// Fetch questions from API
const { data: questionsData } = useQuery(
roadmapQuestionsOptions(roadmapId),
queryClient,
);
// Randomly select 4 questions to display
const defaultQuestions = useMemo(() => {
if (!questionsData?.questions || questionsData.questions.length === 0) {
return [];
}
const shuffled = [...questionsData.questions].sort(
() => 0.5 - Math.random(),
);
return shuffled.slice(0, 4);
}, [questionsData]);
const { data: roadmapDetail, isLoading: isRoadmapDetailLoading } = useQuery(
roadmapJSONOptions(roadmapId),
queryClient,
);
const isAuthenticatedUser = isLoggedIn();
const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const isLimitExceeded =
isAuthenticatedUser && (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const percentageUsed = Math.round(
((tokenUsage?.used || 0) / (tokenUsage?.limit || 0)) * 100,
);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status === 'active';
const totalTopicCount = useMemo(() => {
const allowedTypes = ['topic', 'subtopic', 'todo'];
return (
roadmapDetail?.json?.nodes.filter((node) =>
allowedTypes.includes(node.type || ''),
).length ?? 0
);
}, [roadmapDetail]);
const onSelectTopic = (topicId: string, topicTitle: string) => {
// For now just scroll to bottom and close overlay
const topicSlug = slugify(topicTitle) + '@' + topicId;
window.dispatchEvent(
new CustomEvent('roadmap.node.click', {
detail: {
resourceType: 'roadmap',
resourceId: roadmapId,
topicId: topicSlug,
isCustomResource: false,
},
}),
);
// ensure chat visible
flushSync(() => {
setIsOpen(true);
});
};
const [isChatHistoryLoading, setIsChatHistoryLoading] = useState(true);
const [activeChatHistoryId, setActiveChatHistoryId] = useState<
string | undefined
>();
const { data: chatHistory } = useQuery(
chatHistoryOptions(
activeChatHistoryId,
roadmapAIChatRenderer({
roadmapId,
totalTopicCount,
onSelectTopic,
}),
),
queryClient,
);
const {
aiChatHistory,
isStreamingMessage,
streamedMessage,
showScrollToBottom,
setShowScrollToBottom,
handleChatSubmit,
handleAbort,
scrollToBottom,
clearChat,
setAiChatHistory,
} = useRoadmapAIChat({
activeChatHistoryId,
roadmapId,
totalTopicCount,
scrollareaRef,
onSelectTopic,
onChatHistoryIdChange: (chatHistoryId) => {
setActiveChatHistoryId(chatHistoryId);
},
});
useEffect(() => {
if (!chatHistory) {
return;
}
setAiChatHistory(chatHistory?.messages ?? []);
setIsChatHistoryLoading(false);
setTimeout(() => {
scrollToBottom('instant');
}, 0);
}, [chatHistory]);
useEffect(() => {
if (activeChatHistoryId) {
return;
}
setAiChatHistory([]);
setIsChatHistoryLoading(false);
}, [activeChatHistoryId, setAiChatHistory, setIsChatHistoryLoading]);
useEffect(() => {
lockBodyScroll(isOpen);
}, [isOpen]);
useKeydown('Escape', () => {
setIsOpen(false);
});
useEffect(() => {
// it means user came back to the AI chat from the topic detail
const handleCloseTopicDetail = () => {
lockBodyScroll(isOpen);
};
window.addEventListener(CLOSE_TOPIC_DETAIL_EVENT, handleCloseTopicDetail);
return () => {
window.removeEventListener(
CLOSE_TOPIC_DETAIL_EVENT,
handleCloseTopicDetail,
);
};
}, [isOpen, isPersonalizeOpen]);
function textToJSON(text: string): JSONContent {
return {
type: 'doc',
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
};
}
const submitInput = () => {
if (!isLoggedIn()) {
setIsOpen(false);
showLoginPopup();
return;
}
const trimmed = inputValue.trim();
if (!trimmed) {
return;
}
const json: JSONContent = textToJSON(trimmed);
setInputValue('');
handleChatSubmit(json, isRoadmapDetailLoading);
};
const hasMessages = aiChatHistory.length > 0;
const newTabUrl = `/${roadmapId}/ai${activeChatHistoryId ? `?chatId=${activeChatHistoryId}` : ''}`;
return (
<>
{isOpen && (
<div
onClick={() => {
setIsOpen(false);
}}
className="fixed inset-0 z-50 bg-black opacity-50"
></div>
)}
{showUpgradeModal && (
<UpgradeAccountModal
onClose={() => {
setShowUpgradeModal(false);
}}
/>
)}
{isPersonalizeOpen && (
<UpdatePersonaModal
roadmapId={roadmapId}
onClose={() => {
setIsPersonalizeOpen(false);
}}
/>
)}
<div
className={cn(
'animate-fade-slide-up fixed bottom-5 left-1/2 max-h-[95vh] max-w-[968px] -translate-x-1/4 transform flex-col gap-1.5 overflow-hidden px-4 duration-300 sm:max-h-[50vh] lg:flex',
isOpen ? 'z-91 h-full w-full' : 'z-40 w-auto',
)}
>
{isOpen && (
<>
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-lg bg-white shadow-lg">
{isChatHistoryLoading && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-white">
<div className="flex items-center rounded-md border border-gray-200 py-2 pr-3 pl-2">
<Loader2Icon className="size-4 animate-spin stroke-[2.5] text-gray-400" />
<span className="ml-2 text-sm text-gray-500">
Loading history..
</span>
</div>
</div>
)}
<div className="flex items-center justify-between px-3 py-2">
<div className="flex">
<ChatHeaderButton
icon={<BookOpen className="h-3.5 w-3.5" />}
className="pointer-events-none text-sm"
>
{chatHistory?.title || 'AI Tutor'}
</ChatHeaderButton>
</div>
<div className="flex gap-1.5">
{isPaidUser && activeChatHistoryId && (
<ChatHeaderButton
onClick={() => {
setActiveChatHistoryId(undefined);
inputRef.current?.focus();
}}
icon={<Plus className="h-3.5 w-3.5" />}
className="justify-center rounded-md bg-gray-200 px-2 py-1 text-xs text-black hover:bg-gray-300"
/>
)}
<RoadmapAIChatHistory
roadmapId={roadmapId}
activeChatHistoryId={activeChatHistoryId}
onChatHistoryClick={(chatHistoryId) => {
setIsChatHistoryLoading(true);
setActiveChatHistoryId(chatHistoryId);
setShowScrollToBottom(false);
}}
onDelete={(chatHistoryId) => {
if (activeChatHistoryId === chatHistoryId) {
setActiveChatHistoryId(undefined);
}
}}
onUpgrade={() => {
setShowUpgradeModal(true);
}}
/>
<ChatHeaderButton
href={newTabUrl}
target="_blank"
icon={<SquareArrowOutUpRight className="h-3.5 w-3.5" />}
className="hidden justify-center rounded-md bg-gray-200 px-1 py-1 text-gray-500 hover:bg-gray-300 sm:flex"
/>
<ChatHeaderButton
onClick={() => setIsOpen(false)}
icon={<X className="h-3.5 w-3.5" />}
className="flex items-center justify-center rounded-md bg-red-100 px-1 py-1 text-red-500 hover:bg-red-200"
/>
</div>
</div>
<div
className="flex flex-1 flex-grow flex-col overflow-y-auto px-3 py-2"
ref={scrollareaRef}
>
<div className="flex flex-col gap-2 text-sm">
<RoadmapAIChatCard
role="assistant"
jsx={
<span className="mt-[2px]">
Hey, I am your AI tutor. How can I help you today? 👋
</span>
}
isIntro
/>
{/* Show default questions only when there's no chat history */}
{aiChatHistory.length === 0 &&
defaultQuestions.length > 0 && (
<div className="mt-0.5 mb-1">
<p className="mb-2 text-xs font-normal text-gray-500">
Some questions you might have about this roadmap:
</p>
<div className="flex flex-col justify-end gap-1">
{defaultQuestions.map((question, index) => (
<button
key={`default-question-${index}`}
className="flex h-full self-start rounded-md bg-yellow-500/10 px-3 py-2 text-left text-sm text-black hover:bg-yellow-500/20"
onClick={() => {
if (!isLoggedIn()) {
setIsOpen(false);
showLoginPopup();
return;
}
if (isLimitExceeded) {
setShowUpgradeModal(true);
setIsOpen(false);
return;
}
handleChatSubmit(
textToJSON(question),
isRoadmapDetailLoading,
);
}}
>
{question}
</button>
))}
</div>
</div>
)}
{aiChatHistory.map((chat, index) => (
<Fragment key={`chat-${index}`}>
<RoadmapAIChatCard {...chat} />
</Fragment>
))}
{isStreamingMessage && !streamedMessage && (
<RoadmapAIChatCard role="assistant" html="Thinking..." />
)}
{streamedMessage && (
<RoadmapAIChatCard role="assistant" jsx={streamedMessage} />
)}
</div>
{/* Scroll to bottom button */}
{showScrollToBottom && (
<button
onClick={() => {
scrollToBottom('instant');
setShowScrollToBottom(false);
}}
className="sticky bottom-0 mx-auto mt-2 flex items-center gap-1.5 rounded-full bg-gray-900 px-3 py-1.5 text-xs text-white shadow-lg transition-all hover:bg-gray-800"
>
<ChevronDown className="h-3 w-3" />
Scroll to bottom
</button>
)}
</div>
{isLimitExceeded && (
<UpgradeMessage
onUpgradeClick={() => {
setShowUpgradeModal(true);
setIsOpen(false);
}}
/>
)}
{!isLimitExceeded && (
<>
<div className="flex flex-row justify-between border-t border-gray-200 px-3 pt-2">
<div className="flex gap-2">
<ChatHeaderButton
onClick={() => {
if (!isLoggedIn()) {
setIsOpen(false);
showLoginPopup();
return;
}
setIsPersonalizeOpen(true);
}}
icon={<PersonStanding className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
>
Personalize
</ChatHeaderButton>
{!isPaidUser && isAuthenticatedUser && (
<UsageButton
percentageUsed={percentageUsed}
onUpgradeClick={() => {
setShowUpgradeModal(true);
setIsOpen(false);
}}
/>
)}
</div>
{hasMessages && !isPaidUser && (
<ChatHeaderButton
onClick={() => {
setInputValue('');
clearChat();
}}
icon={<Trash2 className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
>
Clear
</ChatHeaderButton>
)}
</div>
<div className="relative flex items-center text-sm">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
autoFocus
disabled={isLimitExceeded}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (isStreamingMessage) {
return;
}
submitInput();
}
}}
placeholder={
isLimitExceeded
? 'You have reached the usage limit for today..'
: 'Ask me anything about this roadmap...'
}
className={cn(
'w-full resize-none px-3 py-4 outline-none',
isLimitExceeded && 'bg-gray-100 text-gray-400',
)}
/>
<button
className="absolute top-1/2 right-2 -translate-y-1/2 p-1 text-zinc-500 hover:text-black disabled:opacity-50"
disabled={isRoadmapDetailLoading || isLimitExceeded}
onClick={() => {
if (isStreamingMessage) {
handleAbort();
return;
}
submitInput();
}}
>
{isStreamingMessage ? (
<PauseCircleIcon className="h-4 w-4" />
) : (
<SendIcon className="h-4 w-4" />
)}
</button>
</div>
</>
)}
</div>
</>
)}
{!isOpen && (
<button
className={cn(
'relative mx-auto flex w-max flex-shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-stone-900 py-2.5 pr-8 pl-6 text-center text-white shadow-2xl transition-all duration-300 hover:scale-101 hover:bg-stone-800',
)}
onClick={() => {
setIsOpen(true);
setTimeout(() => {
scrollToBottom('instant');
setShowScrollToBottom(false);
}, 0);
}}
>
{!hasMessages ? (
<>
<Wand2 className="h-4 w-4 text-yellow-400" />
<span className="mr-1 text-sm font-semibold text-yellow-400">
AI Tutor
</span>
<span className={'hidden text-white sm:block'}>
Have a question? Type here
</span>
<span className={'block text-white sm:hidden'}>
Ask anything
</span>
</>
) : (
<>
<MessageCirclePlus className="size-5 text-yellow-400" />
<span className="mr-1 text-sm font-medium text-white">
Continue chatting..
</span>
</>
)}
</button>
)}
</div>
</>
);
}

View File

@@ -4,7 +4,6 @@ import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { FineTuneCourse } from './FineTuneCourse';
import { DifficultyDropdown } from '../AITutor/DifficultyDropdown';
import { NatureDropdown, type NatureType } from '../AITutor/NatureDropdown';
import {
clearFineTuneData,
getCourseFineTuneData,
@@ -27,7 +26,6 @@ type AICourseProps = {};
export function AICourse(props: AICourseProps) {
const [keyword, setKeyword] = useState('');
const [difficulty, setDifficulty] = useState<DifficultyLevel>('beginner');
const [nature, setNature] = useState<NatureType>('course');
const [hasFineTuneData, setHasFineTuneData] = useState(false);
const [about, setAbout] = useState('');
@@ -83,11 +81,7 @@ export function AICourse(props: AICourseProps) {
});
}
if (nature === 'course') {
window.location.href = `/ai/course?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}&nature=${nature}`;
} else {
window.location.href = `/ai/document?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}&nature=${nature}`;
}
window.location.href = `/ai/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}`;
}
return (
@@ -137,7 +131,6 @@ export function AICourse(props: AICourseProps) {
<div className="flex flex-col items-start justify-between gap-2 px-4 pb-4 md:flex-row md:items-center">
<div className="flex flex-row items-center gap-2">
<div className="flex flex-row gap-2">
<NatureDropdown value={nature} onChange={setNature} />
<DifficultyDropdown
value={difficulty}
onChange={setDifficulty}
@@ -155,6 +148,7 @@ export function AICourse(props: AICourseProps) {
id="fine-tune-checkbox"
/>
Explain more
<span className="hidden md:inline"> for a better course</span>
</label>
</div>
@@ -175,6 +169,7 @@ export function AICourse(props: AICourseProps) {
<FineTuneCourse
hasFineTuneData={hasFineTuneData}
setHasFineTuneData={setHasFineTuneData}
about={about}
goal={goal}
customInstructions={customInstructions}

View File

@@ -2,24 +2,17 @@ import type { AICourseWithLessonCount } from '../../queries/ai-course';
import type { DifficultyLevel } from './AICourse';
import { BookOpen } from 'lucide-react';
import { AICourseActions } from './AICourseActions';
import { getRelativeTimeString } from '../../lib/date';
import { cn } from '../../lib/classname';
type AICourseCardProps = {
course: AICourseWithLessonCount;
showActions?: boolean;
showProgress?: boolean;
variant?: 'row' | 'column';
};
export function AICourseCard(props: AICourseCardProps) {
const {
course,
showActions = true,
showProgress = true,
variant = 'row',
} = props;
const { course, showActions = true, showProgress = true } = props;
// Map difficulty to color
const difficultyColor =
{
beginner: 'text-green-700',
@@ -27,65 +20,49 @@ export function AICourseCard(props: AICourseCardProps) {
advanced: 'text-purple-700',
}[course.difficulty as DifficultyLevel] || 'text-gray-700';
const modulesCount = course.modules?.length || 0;
// Calculate progress percentage
const totalTopics = course.lessonCount || 0;
const completedTopics = course.done?.length || 0;
const progressPercentage =
totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
const updatedAgo = getRelativeTimeString(course?.updatedAt);
return (
<div className="relative flex flex-grow">
<div className="relative flex flex-grow flex-col">
<a
href={`/ai/${course.slug}`}
className={cn(
'group relative flex h-full w-full gap-3 overflow-hidden rounded-lg border border-gray-200 bg-white p-4 text-left transition-all hover:border-gray-300 hover:bg-gray-50 sm:gap-4',
variant === 'column' && 'flex-col',
variant === 'row' && 'flex-row sm:flex-row sm:items-center',
)}
className="hover:border-gray-3 00 group relative flex h-full min-h-[140px] w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4 text-left transition-all hover:bg-gray-50"
>
{/* Title and difficulty section */}
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
<span
className={`rounded-full text-xs font-medium capitalize opacity-80 ${difficultyColor}`}
>
{course.difficulty}
</span>
</div>
<h3 className="line-clamp-2 text-base font-semibold text-balance text-gray-900">
{course.title
?.replace(": A Beginner's Guide", '')
?.replace(' for beginners', '')
?.replace(': A Comprehensive Guide', '')}
</h3>
<div className="flex items-center justify-between">
<span
className={`rounded-full text-xs font-medium capitalize opacity-80 ${difficultyColor}`}
>
{course.difficulty}
</span>
</div>
{/* Course stats section */}
<div className="mt-7 flex items-center gap-4 sm:gap-4">
<div className="hidden items-center text-xs text-gray-600 sm:flex">
<h3 className="my-2 text-base font-semibold text-gray-900">
{course.title}
</h3>
<div className="mt-auto flex items-center justify-between pt-2">
<div className="flex items-center text-xs text-gray-600">
<BookOpen className="mr-1 h-3.5 w-3.5" />
<span>{modulesCount} modules</span>
<span>{totalTopics} lessons</span>
</div>
<div className="flex items-center gap-2 text-xs text-gray-600">
{showProgress && totalTopics > 0 && (
<div className="flex items-center">
<BookOpen className="mr-1 h-3.5 w-3.5" />
<span>{totalTopics} lessons</span>
<div className="mr-2 h-1.5 w-16 overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full bg-blue-600"
style={{ width: `${progressPercentage}%` }}
/>
</div>
<span className="text-xs font-medium text-gray-700">
{progressPercentage}%
</span>
</div>
{showProgress && totalTopics > 0 && (
<>
<span className="hidden text-gray-400 sm:inline"></span>
<div className="flex items-center">
<span className="flex items-center text-xs font-medium text-gray-700">
{progressPercentage}% complete
</span>
</div>
</>
)}
</div>
)}
</div>
</a>

View File

@@ -25,7 +25,6 @@ import { AICourseFooter } from './AICourseFooter';
import { ForkCourseAlert } from './ForkCourseAlert';
import { ForkCourseConfirmation } from './ForkCourseConfirmation';
import { useAuth } from '../../hooks/use-auth';
import { getPercentage } from '../../lib/number';
type AICourseContentProps = {
courseSlug?: string;
@@ -135,9 +134,8 @@ export function AICourseContent(props: AICourseContentProps) {
);
const totalDoneLessons = (course?.done || []).length;
const finishedPercentage = getPercentage(
totalDoneLessons,
totalCourseLessons,
const finishedPercentage = Math.round(
(totalDoneLessons / totalCourseLessons) * 100,
);
const modals = (
@@ -315,7 +313,7 @@ export function AICourseContent(props: AICourseContentProps) {
</span>
)}
{finishedPercentage > 0 && !isLoading && (
{finishedPercentage > 0 && (
<>
<span className="text-gray-400"></span>
<span className="font-medium text-green-600">

View File

@@ -1,4 +1,3 @@
import '../RoadmapAIChat/RoadmapAIChat.css';
import { useQuery } from '@tanstack/react-query';
import {
BookOpen,

View File

@@ -5,12 +5,10 @@ import { useDebounceValue } from '../../hooks/use-debounce';
type AICourseSearchProps = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
};
export function AICourseSearch(props: AICourseSearchProps) {
const { value: defaultValue, onChange, placeholder, disabled } = props;
const { value: defaultValue, onChange } = props;
const [searchTerm, setSearchTerm] = useState(defaultValue);
const debouncedSearchTerm = useDebounceValue(searchTerm, 500);
@@ -32,17 +30,16 @@ export function AICourseSearch(props: AICourseSearchProps) {
}, [debouncedSearchTerm]);
return (
<div className="relative mb-4">
<div className="relative w-64 max-sm:hidden">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<SearchIcon className="h-4 w-4 text-gray-400" />
</div>
<input
type="text"
className="block w-full rounded-lg border border-gray-200 bg-white py-3 pr-3 pl-10 leading-5 placeholder-gray-500 focus:border-gray-300 focus:ring-blue-500 focus:outline-hidden disabled:opacity-70 sm:text-sm"
placeholder={placeholder || 'Search courses...'}
className="block w-full rounded-md border border-gray-200 bg-white py-1.5 pl-10 pr-3 leading-5 placeholder-gray-500 focus:border-gray-300 focus:outline-hidden focus:ring-blue-500 disabled:opacity-70 sm:text-sm"
placeholder="Search courses..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
disabled={disabled}
/>
</div>
);

View File

@@ -1,6 +1,3 @@
import { useId } from 'react';
import { cn } from '../../lib/classname';
type QuestionProps = {
label: string;
placeholder: string;
@@ -11,18 +8,13 @@ type QuestionProps = {
function Question(props: QuestionProps) {
const { label, placeholder, value, onChange, autoFocus = false } = props;
const questionId = useId();
return (
<div className="flex flex-col">
<label
htmlFor={questionId}
className="border-y bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700"
>
<label className="border-y bg-gray-100 px-4 py-2.5 text-sm font-medium text-gray-700">
{label}
</label>
<textarea
id={questionId}
placeholder={placeholder}
className="min-h-[80px] w-full resize-none px-4 py-3 text-gray-700 placeholder:text-gray-400 focus:outline-hidden"
value={value}
@@ -39,10 +31,10 @@ type FineTuneCourseProps = {
goal: string;
customInstructions: string;
setHasFineTuneData: (hasMetadata: boolean) => void;
setAbout: (about: string) => void;
setGoal: (goal: string) => void;
setCustomInstructions: (customInstructions: string) => void;
className?: string;
};
export function FineTuneCourse(props: FineTuneCourseProps) {
@@ -54,7 +46,7 @@ export function FineTuneCourse(props: FineTuneCourseProps) {
setAbout,
setGoal,
setCustomInstructions,
className,
setHasFineTuneData,
} = props;
if (!hasFineTuneData) {
@@ -62,7 +54,7 @@ export function FineTuneCourse(props: FineTuneCourseProps) {
}
return (
<div className={cn('mt-0 flex flex-col', className)}>
<div className="mt-0 flex flex-col">
<Question
label="Tell us about yourself"
placeholder="e.g. I am a frontend developer and have good knowledge of HTML, CSS, and JavaScript."

View File

@@ -1,19 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import { BookOpen, Loader2 } from 'lucide-react';
import { BookOpen } from 'lucide-react';
import { useEffect, useState } from 'react';
import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import {
listUserAiCoursesOptions,
type ListUserAiCoursesQuery,
} from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AITutorHeader } from '../AITutor/AITutorHeader';
import { AITutorTallMessage } from '../AITutor/AITutorTallMessage';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { Pagination } from '../Pagination/Pagination';
import { AICourseCard } from './AICourseCard';
import { AICourseSearch } from './AICourseSearch';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { AILoadingState } from '../AITutor/AILoadingState';
export function UserCoursesList() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -58,8 +60,28 @@ export function UserCoursesList() {
}
}, [pageState]);
const isUserAuthenticated = isLoggedIn();
const isAnyLoading = isUserAiCoursesLoading || isInitialLoading;
if (isUserAiCoursesLoading || isInitialLoading) {
return (
<AILoadingState
title="Loading your courses"
subtitle="This may take a moment..."
/>
);
}
if (!isLoggedIn()) {
return (
<AITutorTallMessage
title="Sign up or login"
subtitle="Takes 2s to sign up and generate your first course."
icon={BookOpen}
buttonText="Sign up or Login"
onButtonClick={() => {
showLoginPopup();
}}
/>
);
}
return (
<>
@@ -67,78 +89,60 @@ export function UserCoursesList() {
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
disabled={isAnyLoading}
/>
<AITutorHeader
title="Your Courses"
onUpgradeClick={() => setShowUpgradePopup(true)}
>
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</AITutorHeader>
{isAnyLoading && (
<p className="mb-4 flex flex-row items-center gap-2 text-sm text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading your courses...
</p>
{(isUserAiCoursesLoading || isInitialLoading) && (
<AILoadingState
title="Loading your courses"
subtitle="This may take a moment..."
/>
)}
{!isAnyLoading && (
<>
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated
? `You have generated ${userAiCourses?.totalCount} courses so far.`
: 'Sign up or login to generate your first course. Takes 2s to do so.'}
</p>
{!isUserAiCoursesLoading && !isInitialLoading && courses.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3">
{courses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
</div>
{isUserAuthenticated && !isAnyLoading && courses.length > 0 && (
<div className="flex flex-col gap-2">
{courses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
<Pagination
totalCount={userAiCourses?.totalCount || 0}
totalPages={userAiCourses?.totalPages || 0}
currPage={Number(userAiCourses?.currPage || 1)}
perPage={Number(userAiCourses?.perPage || 10)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
<Pagination
totalCount={userAiCourses?.totalCount || 0}
totalPages={userAiCourses?.totalPages || 0}
currPage={Number(userAiCourses?.currPage || 1)}
perPage={Number(userAiCourses?.perPage || 10)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{!isAnyLoading && courses.length === 0 && (
<AITutorTallMessage
title={
isUserAuthenticated ? 'No courses found' : 'Sign up or login'
}
subtitle={
isUserAuthenticated
? "You haven't generated any courses yet."
: 'Takes 2s to sign up and generate your first course.'
}
icon={BookOpen}
buttonText={
isUserAuthenticated
? 'Create your first course'
: 'Sign up or login'
}
onButtonClick={() => {
if (isUserAuthenticated) {
window.location.href = '/ai';
} else {
showLoginPopup();
}
}}
/>
)}
</>
{!isUserAiCoursesLoading && !isInitialLoading && courses.length === 0 && (
<AITutorTallMessage
title="No courses found"
subtitle="You haven't generated any courses yet."
icon={BookOpen}
buttonText="Create your first course"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
)}
</>
);

View File

@@ -1,231 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { ExternalLink } from 'lucide-react';
import { useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import { generateGuide } from '../../helper/generate-ai-guide';
import { shuffle } from '../../helper/shuffle';
import { useToast } from '../../hooks/use-toast';
import { isLoggedIn } from '../../lib/jwt';
import {
aiGuideSuggestionsOptions,
getAiGuideOptions,
} from '../../queries/ai-guide';
import { queryClient } from '../../stores/query-client';
import { AITutorLayout } from '../AITutor/AITutorLayout';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AIGuideChat } from './AIGuideChat';
import { AIGuideContent } from './AIGuideContent';
import { GenerateAIGuide } from './GenerateAIGuide';
type AIGuideProps = {
guideSlug?: string;
};
export function AIGuide(props: AIGuideProps) {
const { guideSlug: defaultGuideSlug } = props;
const [guideSlug, setGuideSlug] = useState(defaultGuideSlug);
const toast = useToast();
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [isRegenerating, setIsRegenerating] = useState(false);
const [regeneratedHtml, setRegeneratedHtml] = useState<string | null>(null);
// only fetch the guide if the guideSlug is provided
// otherwise we are still generating the guide
const { data: aiGuide, isLoading: isLoadingBySlug } = useQuery(
getAiGuideOptions(guideSlug),
queryClient,
);
const { data: aiGuideSuggestions, isLoading: isAiGuideSuggestionsLoading } =
useQuery(
{
...aiGuideSuggestionsOptions(guideSlug),
enabled: !!guideSlug && !!isLoggedIn(),
},
queryClient,
);
const randomQuestions = useMemo(() => {
return shuffle(aiGuideSuggestions?.questions || []).slice(0, 4);
}, [aiGuideSuggestions]);
const relatedTopics = useMemo(() => {
return shuffle(aiGuideSuggestions?.relatedTopics || []).slice(0, 2);
}, [aiGuideSuggestions]);
const deepDiveTopics = useMemo(() => {
return shuffle(aiGuideSuggestions?.deepDiveTopics || []).slice(0, 2);
}, [aiGuideSuggestions]);
const handleRegenerate = async (prompt?: string) => {
flushSync(() => {
setIsRegenerating(true);
setRegeneratedHtml(null);
});
queryClient.cancelQueries(getAiGuideOptions(guideSlug));
queryClient.setQueryData(getAiGuideOptions(guideSlug).queryKey, (old) => {
if (!old) {
return old;
}
return {
...old,
content: '',
html: '',
};
});
await generateGuide({
slug: aiGuide?.slug || '',
term: aiGuide?.keyword || '',
depth: aiGuide?.depth || '',
prompt,
onStreamingChange: setIsRegenerating,
onHtmlChange: setRegeneratedHtml,
onFinish: () => {
setIsRegenerating(false);
queryClient.invalidateQueries(getAiGuideOptions(guideSlug));
},
isForce: true,
onError: (error) => {
toast.error(error);
},
});
};
return (
<AITutorLayout
wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden bg-white"
containerClassName="h-[calc(100vh-49px)] overflow-hidden relative"
>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
<div className="grow overflow-y-auto p-4 pt-0">
{guideSlug && (
<AIGuideContent
html={regeneratedHtml || aiGuide?.html || ''}
onRegenerate={handleRegenerate}
isLoading={isLoadingBySlug || isRegenerating}
/>
)}
{!guideSlug && <GenerateAIGuide onGuideSlugChange={setGuideSlug} />}
{aiGuide && !isRegenerating && (
<div className="mx-auto mt-12 mb-12 max-w-4xl">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<ListSuggestions
title="Related Topics"
suggestions={relatedTopics}
depth="essentials"
isLoading={isAiGuideSuggestionsLoading}
currentGuideTitle={aiGuide.title}
/>
<ListSuggestions
title="Dive Deeper"
suggestions={deepDiveTopics}
depth="detailed"
isLoading={isAiGuideSuggestionsLoading}
currentGuideTitle={aiGuide.title}
/>
</div>
</div>
)}
</div>
<AIGuideChat
guideSlug={guideSlug}
isGuideLoading={!aiGuide}
onUpgrade={() => setShowUpgradeModal(true)}
randomQuestions={randomQuestions}
isQuestionsLoading={isAiGuideSuggestionsLoading}
/>
</AITutorLayout>
);
}
type ListSuggestionsProps = {
currentGuideTitle?: string;
title: string;
suggestions: string[];
depth: string;
isLoading: boolean;
};
export function ListSuggestions(props: ListSuggestionsProps) {
const { title, suggestions, depth, isLoading, currentGuideTitle } = props;
return (
<div className="group relative overflow-hidden rounded-xl border border-gray-300 bg-linear-to-br from-gray-100 to-gray-50 shadow-xs transition-all duration-200">
<div className="border-b border-gray-200 bg-white px-5 py-4">
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
<p className="mt-1 text-sm text-gray-600">
{depth === 'essentials'
? 'Explore related concepts to expand your knowledge'
: 'Take a deeper dive into specific areas'}
</p>
</div>
<div className="p-5">
{isLoading && (
<div className="space-y-3">
{[1, 2].map((i) => (
<div
key={i}
className="h-10 w-full animate-pulse rounded-lg bg-white"
></div>
))}
</div>
)}
{!isLoading && suggestions?.length > 0 && (
<div className="space-y-2">
{suggestions.map((topic) => {
const topicTerm =
depth === 'essentials'
? `I have covered the basics of ${currentGuideTitle} and want to learn more about ${topic}`
: `I have covered the basics of ${currentGuideTitle} and want to dive deeper into ${topic}`;
const url = `/ai/guide?term=${encodeURIComponent(topicTerm)}&depth=${depth}&id=&format=guide`;
return (
<a
key={topic}
href={url}
target="_blank"
className="group/item flex items-center justify-between rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm font-medium text-gray-700 transition-all duration-200 hover:border-gray-300 hover:bg-gray-50"
>
<span className="flex-1 truncate group-hover/item:text-gray-900">
{topic}
</span>
<ExternalLink className="ml-2 size-4 text-gray-400 group-hover/item:text-gray-600" />
</a>
);
})}
</div>
)}
{!isLoading && suggestions?.length === 0 && (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="mb-3 rounded-full bg-gray-100 p-3">
<svg
className="h-6 w-6 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<p className="text-sm text-gray-500">No suggestions available</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,381 +0,0 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { useChat, type ChatMessage } from '../../hooks/use-chat';
import { RoadmapAIChatCard } from '../RoadmapAIChat/RoadmapAIChatCard';
import {
ArrowDownIcon,
BotIcon,
LockIcon,
MessageCircleIcon,
PauseCircleIcon,
SendIcon,
Trash2Icon,
XIcon,
} from 'lucide-react';
import { ChatHeaderButton } from '../FrameRenderer/RoadmapFloatingChat';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { flushSync } from 'react-dom';
import { markdownToHtml } from '../../lib/markdown';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { LoadingChip } from '../LoadingChip';
import { getTailwindScreenDimension } from '../../lib/is-mobile';
type AIGuideChatProps = {
guideSlug?: string;
isGuideLoading?: boolean;
onUpgrade?: () => void;
isQuestionsLoading?: boolean;
randomQuestions?: string[];
};
export function AIGuideChat(props: AIGuideChatProps) {
const {
guideSlug,
isGuideLoading,
onUpgrade,
randomQuestions,
isQuestionsLoading,
} = props;
const scrollareaRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState('');
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [isChatOpen, setIsChatOpen] = useState(true);
const [isMobile, setIsMobile] = useState(false);
const {
data: tokenUsage,
isLoading: isTokenUsageLoading,
refetch: refetchTokenUsage,
} = useQuery(getAiCourseLimitOptions(), queryClient);
const {
data: userBillingDetails,
isLoading: isBillingDetailsLoading,
refetch: refetchBillingDetails,
} = useQuery(billingDetailsOptions(), queryClient);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
const {
messages,
status,
streamedMessageHtml,
sendMessages,
setMessages,
stop,
} = useChat({
endpoint: `${import.meta.env.PUBLIC_API_URL}/v1-ai-guide-chat`,
onError: (error) => {
console.error(error);
},
data: {
guideSlug,
},
onFinish: () => {
refetchTokenUsage();
},
});
const scrollToBottom = useCallback(
(behavior: 'smooth' | 'instant' = 'smooth') => {
scrollareaRef.current?.scrollTo({
top: scrollareaRef.current.scrollHeight,
behavior,
});
},
[scrollareaRef],
);
const isStreamingMessage = status === 'streaming';
const hasMessages = messages.length > 0;
const handleSubmitInput = useCallback(
(defaultInputValue?: string) => {
const message = defaultInputValue || inputValue;
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isStreamingMessage) {
return;
}
const newMessages: ChatMessage[] = [
...messages,
{
role: 'user',
content: message,
html: markdownToHtml(message),
},
];
flushSync(() => {
setMessages(newMessages);
});
sendMessages(newMessages);
setInputValue('');
},
[inputValue, isStreamingMessage, messages, sendMessages, setMessages],
);
const checkScrollPosition = useCallback(() => {
const scrollArea = scrollareaRef.current;
if (!scrollArea) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = scrollArea;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; // 50px threshold
setShowScrollToBottom(!isAtBottom && messages.length > 0);
}, [messages.length]);
useEffect(() => {
const scrollArea = scrollareaRef.current;
if (!scrollArea) {
return;
}
scrollArea.addEventListener('scroll', checkScrollPosition);
return () => scrollArea.removeEventListener('scroll', checkScrollPosition);
}, [checkScrollPosition]);
const isLoading =
isGuideLoading || isTokenUsageLoading || isBillingDetailsLoading;
useLayoutEffect(() => {
const deviceType = getTailwindScreenDimension();
const isMediumSize = ['sm', 'md'].includes(deviceType);
if (!isMediumSize) {
const storedState = localStorage.getItem('chat-history-sidebar-open');
setIsChatOpen(storedState === null ? true : storedState === 'true');
} else {
setIsChatOpen(!isMediumSize);
}
setIsMobile(isMediumSize);
}, []);
useEffect(() => {
if (!isMobile) {
localStorage.setItem('chat-history-sidebar-open', isChatOpen.toString());
}
}, [isChatOpen, isMobile]);
if (!isChatOpen) {
return (
<div className="absolute inset-x-0 bottom-0 flex justify-center p-2">
<button
className="flex items-center justify-center gap-2 rounded-full bg-black px-4 py-2 text-white shadow"
onClick={() => {
setIsChatOpen(true);
}}
>
<MessageCircleIcon className="h-4 w-4" />
<span className="text-sm">Open Chat</span>
</button>
</div>
);
}
return (
<div className="absolute inset-0 flex h-full w-full max-w-full flex-col overflow-hidden border-l border-gray-200 bg-white md:relative md:max-w-[40%]">
<div className="flex items-center justify-between gap-2 border-b border-gray-200 bg-white p-2">
<h2 className="flex items-center gap-2 text-sm font-medium">
<BotIcon className="h-4 w-4" />
AI Guide
</h2>
<button
className="mr-2 flex size-5 items-center justify-center rounded-md text-gray-500 hover:bg-gray-300 md:hidden"
onClick={() => {
setIsChatOpen(false);
}}
>
<XIcon className="h-3.5 w-3.5" />
</button>
</div>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<LoadingChip message="Loading..." />
</div>
)}
{!isLoading && (
<>
<div className="relative grow overflow-y-auto" ref={scrollareaRef}>
<div className="absolute inset-0 flex flex-col">
<div className="relative flex grow flex-col justify-end">
<div className="flex flex-col justify-end gap-2 px-3 py-2">
<RoadmapAIChatCard
role="assistant"
html="Hello, how can I help you today?"
isIntro
/>
{isQuestionsLoading && (
<div className="flex flex-col gap-2">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="h-[48px] w-full animate-pulse rounded-lg bg-gray-200"
></div>
))}
</div>
)}
{!isQuestionsLoading &&
randomQuestions &&
randomQuestions.length > 0 &&
messages.length === 0 && (
<div className="space-y-2">
<p className="mb-2 text-xs font-normal text-gray-500">
Some questions you might have about this lesson.
</p>
<div className="space-y-1">
{randomQuestions?.map((question) => {
return (
<button
key={`chat-${question}`}
className="flex h-full self-start rounded-md bg-yellow-500/10 px-3 py-2 text-left text-sm text-black hover:bg-yellow-500/20"
onClick={() => {
handleSubmitInput(question);
}}
>
{question}
</button>
);
})}
</div>
</div>
)}
{messages.map((chat, index) => {
return (
<RoadmapAIChatCard key={`chat-${index}`} {...chat} />
);
})}
{status === 'streaming' && !streamedMessageHtml && (
<RoadmapAIChatCard role="assistant" html="Thinking..." />
)}
{status === 'streaming' && streamedMessageHtml && (
<RoadmapAIChatCard
role="assistant"
html={streamedMessageHtml}
/>
)}
</div>
</div>
</div>
</div>
{(hasMessages || showScrollToBottom) && (
<div className="flex flex-row justify-between gap-2 border-t border-gray-200 px-3 py-2">
<ChatHeaderButton
icon={<Trash2Icon className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
onClick={() => {
setMessages([]);
}}
>
Clear
</ChatHeaderButton>
{showScrollToBottom && (
<ChatHeaderButton
icon={<ArrowDownIcon className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
onClick={() => {
scrollToBottom('smooth');
}}
>
Scroll to bottom
</ChatHeaderButton>
)}
</div>
)}
<div className="relative flex items-center border-t border-gray-200 text-sm">
{isLimitExceeded && isLoggedIn() && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon
className="size-4 cursor-not-allowed"
strokeWidth={2.5}
/>
<p className="cursor-not-allowed">
Limit reached for today
{isPaidUser ? '. Please wait until tomorrow.' : ''}
</p>
{!isPaidUser && (
<button
onClick={() => {
onUpgrade?.();
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Upgrade for more
</button>
)}
</div>
)}
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (isStreamingMessage) {
return;
}
handleSubmitInput();
}
}}
placeholder="Ask me anything about this guide..."
className="w-full resize-none px-3 py-4 outline-none"
/>
<button
className="absolute top-1/2 right-2 -translate-y-1/2 p-1 text-zinc-500 hover:text-black disabled:opacity-50"
onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (status !== 'idle') {
stop();
return;
}
handleSubmitInput();
}}
>
{isStreamingMessage ? (
<PauseCircleIcon className="h-4 w-4" />
) : (
<SendIcon className="h-4 w-4" />
)}
</button>
</div>
</>
)}
</div>
);
}

View File

@@ -1,131 +0,0 @@
.prose ul li > code,
.prose ol li > code,
p code,
a > code,
strong > code,
em > code,
h1 > code,
h2 > code,
h3 > code {
background: #ebebeb !important;
color: currentColor !important;
font-size: 14px;
font-weight: normal !important;
}
.course-ai-content.course-content.prose ul li > code,
.course-ai-content.course-content.prose ol li > code,
.course-ai-content.course-content.prose p code,
.course-ai-content.course-content.prose a > code,
.course-ai-content.course-content.prose strong > code,
.course-ai-content.course-content.prose em > code,
.course-ai-content.course-content.prose h1 > code,
.course-ai-content.course-content.prose h2 > code,
.course-ai-content.course-content.prose h3 > code,
.course-notes-content.prose ul li > code,
.course-notes-content.prose ol li > code,
.course-notes-content.prose p code,
.course-notes-content.prose a > code,
.course-notes-content.prose strong > code,
.course-notes-content.prose em > code,
.course-notes-content.prose h1 > code,
.course-notes-content.prose h2 > code,
.course-notes-content.prose h3 > code {
font-size: 12px !important;
}
.course-ai-content pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
.course-ai-content pre::-webkit-scrollbar {
display: none;
}
.course-ai-content pre,
.course-notes-content pre {
overflow: scroll;
font-size: 15px;
margin: 10px 0;
}
.prose ul li > code:before,
p > code:before,
.prose ul li > code:after,
.prose ol li > code:before,
p > code:before,
.prose ol li > code:after,
.course-content h1 > code:after,
.course-content h1 > code:before,
.course-content h2 > code:after,
.course-content h2 > code:before,
.course-content h3 > code:after,
.course-content h3 > code:before,
.course-content h4 > code:after,
.course-content h4 > code:before,
p > code:after,
a > code:after,
a > code:before {
content: '' !important;
}
.course-content.prose ul li > code,
.course-content.prose ol li > code,
.course-content p code,
.course-content a > code,
.course-content strong > code,
.course-content em > code,
.course-content h1 > code,
.course-content h2 > code,
.course-content h3 > code,
.course-content table code {
background: #f4f4f5 !important;
border: 1px solid #282a36 !important;
color: #282a36 !important;
padding: 2px 4px;
border-radius: 5px;
font-size: 16px !important;
white-space: pre;
font-weight: normal;
}
.course-content blockquote {
font-style: normal;
}
.course-content.prose blockquote h1,
.course-content.prose blockquote h2,
.course-content.prose blockquote h3,
.course-content.prose blockquote h4 {
font-style: normal;
margin-bottom: 8px;
}
.course-content.prose ul li > code:before,
.course-content p > code:before,
.course-content.prose ul li > code:after,
.course-content p > code:after,
.course-content h2 > code:after,
.course-content h2 > code:before,
.course-content table code:before,
.course-content table code:after,
.course-content a > code:after,
.course-content a > code:before,
.course-content h2 code:after,
.course-content h2 code:before,
.course-content h2 code:after,
.course-content h2 code:before {
content: '' !important;
}
.course-content table {
border-collapse: collapse;
border: 1px solid black;
border-radius: 5px;
}
.course-content table td,
.course-content table th {
padding: 5px 10px;
}

View File

@@ -1,40 +0,0 @@
import './AIGuideContent.css';
import { AIGuideRegenerate } from './AIGuideRegenerate';
import { cn } from '../../lib/classname';
import { LoadingChip } from '../LoadingChip';
type AIGuideContentProps = {
html: string;
onRegenerate?: (prompt?: string) => void;
isLoading?: boolean;
};
export function AIGuideContent(props: AIGuideContentProps) {
const { html, onRegenerate, isLoading } = props;
return (
<div
className={cn(
'relative mx-auto w-full max-w-4xl',
isLoading && 'min-h-full',
)}
>
<div
className="course-content prose-h1:leading-[1.15] prose prose-lg prose-headings:mb-3 prose-headings:mt-8 prose-blockquote:font-normal prose-pre:rounded-2xl prose-pre:text-lg prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800 max-lg:prose-h2:mt-3 max-lg:prose-h2:text-lg max-lg:prose-h3:text-base max-lg:prose-pre:px-3 max-lg:prose-pre:text-sm mt-8 max-w-full text-black max-lg:mt-4 max-lg:text-base [&>h1]:text-balance"
dangerouslySetInnerHTML={{ __html: html }}
/>
{isLoading && !html && (
<div className="absolute inset-0 flex items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
)}
{onRegenerate && !isLoading && (
<div className="absolute top-4 right-4">
<AIGuideRegenerate onRegenerate={onRegenerate} />
</div>
)}
</div>
);
}

View File

@@ -1,88 +0,0 @@
import { PenSquare, RefreshCcw } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { cn } from '../../lib/classname';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { ModifyCoursePrompt } from '../GenerateCourse/ModifyCoursePrompt';
type AIGuideRegenerateProps = {
onRegenerate: (prompt?: string) => void;
};
export function AIGuideRegenerate(props: AIGuideRegenerateProps) {
const { onRegenerate } = props;
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showPromptModal, setShowPromptModal] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOutsideClick(ref, () => setIsDropdownVisible(false));
return (
<>
{showUpgradeModal && (
<UpgradeAccountModal
onClose={() => {
setShowUpgradeModal(false);
}}
/>
)}
{showPromptModal && (
<ModifyCoursePrompt
description="Pass additional information to the AI to generate a guide."
onClose={() => setShowPromptModal(false)}
onSubmit={(prompt) => {
setShowPromptModal(false);
onRegenerate(prompt);
}}
/>
)}
<div ref={ref} className="relative flex items-stretch">
<button
className={cn('rounded-md px-2.5 text-gray-400 hover:text-black', {
'text-black': isDropdownVisible,
})}
onClick={() => setIsDropdownVisible(!isDropdownVisible)}
>
<PenSquare className="text-current" size={16} strokeWidth={2.5} />
</button>
{isDropdownVisible && (
<div className="absolute top-full right-0 min-w-[170px] translate-y-1 overflow-hidden rounded-md border border-gray-200 bg-white shadow-md">
<button
onClick={() => {
setIsDropdownVisible(false);
onRegenerate();
}}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
>
<RefreshCcw
size={16}
className="text-gray-400"
strokeWidth={2.5}
/>
Regenerate
</button>
<button
onClick={() => {
setIsDropdownVisible(false);
setShowPromptModal(true);
}}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
>
<PenSquare
size={16}
className="text-gray-400"
strokeWidth={2.5}
/>
Modify Prompt
</button>
</div>
)}
</div>
</>
);
}

View File

@@ -1,142 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { generateGuide } from '../../helper/generate-ai-guide';
import { getCourseFineTuneData } from '../../lib/ai';
import { getUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { AIGuideContent } from './AIGuideContent';
import { queryClient } from '../../stores/query-client';
import { getAiGuideOptions } from '../../queries/ai-guide';
import { LoadingChip } from '../LoadingChip';
type GenerateAIGuideProps = {
onGuideSlugChange?: (guideSlug: string) => void;
};
export function GenerateAIGuide(props: GenerateAIGuideProps) {
const { onGuideSlugChange } = props;
const [isLoading, setIsLoading] = useState(true);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState('');
const [content, setContent] = useState('');
const [html, setHtml] = useState('');
const htmlRef = useRef<string>('');
useEffect(() => {
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsDepth = params?.depth;
const paramsSrc = params?.src || 'search';
if (!paramsTerm || !paramsDepth) {
return;
}
let paramsGoal = '';
let paramsAbout = '';
let paramsCustomInstructions = '';
const sessionId = params?.id;
if (sessionId) {
const fineTuneData = getCourseFineTuneData(sessionId);
if (fineTuneData) {
paramsGoal = fineTuneData.goal;
paramsAbout = fineTuneData.about;
paramsCustomInstructions = fineTuneData.customInstructions;
}
}
handleGenerateDocument({
term: paramsTerm,
depth: paramsDepth,
instructions: paramsCustomInstructions,
goal: paramsGoal,
about: paramsAbout,
src: paramsSrc,
});
}, []);
const handleGenerateDocument = async (options: {
term: string;
depth: string;
instructions?: string;
goal?: string;
about?: string;
isForce?: boolean;
prompt?: string;
src?: string;
}) => {
const { term, depth, isForce, prompt, instructions, goal, about, src } =
options;
if (!isLoggedIn()) {
window.location.href = '/ai';
return;
}
await generateGuide({
term,
depth,
onDetailsChange: (details) => {
const { guideId, guideSlug, creatorId, title } = details;
const guideData = {
_id: guideId,
userId: creatorId,
title,
html: htmlRef.current,
keyword: term,
depth,
content,
tokens: {
prompt: 0,
completion: 0,
total: 0,
},
relatedTopics: [],
deepDiveTopics: [],
questions: [],
viewCount: 0,
lastVisitedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
queryClient.setQueryData(
getAiGuideOptions(guideSlug).queryKey,
guideData,
);
onGuideSlugChange?.(guideSlug);
window.history.replaceState(null, '', `/ai/guide/${guideSlug}`);
},
onLoadingChange: setIsLoading,
onError: setError,
instructions,
goal,
about,
isForce,
prompt,
src,
onHtmlChange: (html) => {
htmlRef.current = html;
setHtml(html);
},
onStreamingChange: setIsStreaming,
});
};
if (error) {
return <div className="text-red-500">{error}</div>;
}
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
);
}
return <AIGuideContent html={html} />;
}

View File

@@ -1,84 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { queryClient } from '../../stores/query-client';
import { AIGuideContent } from './AIGuideContent';
import { getAiGuideOptions } from '../../queries/ai-guide';
type GetAIGuideProps = {
slug: string;
};
export function GetAIGuide(props: GetAIGuideProps) {
const { slug: documentSlug } = props;
const [isLoading, setIsLoading] = useState(true);
const [isRegenerating, setIsRegenerating] = useState(false);
const [error, setError] = useState('');
const { data: aiGuide, error: queryError } = useQuery(
{
...getAiGuideOptions(documentSlug),
enabled: !!documentSlug,
},
queryClient,
);
useEffect(() => {
if (!aiGuide) {
return;
}
setIsLoading(false);
}, [aiGuide]);
useEffect(() => {
if (!queryError) {
return;
}
setIsLoading(false);
setError(queryError.message);
}, [queryError]);
const handleRegenerateDocument = async (prompt?: string) => {
// if (!aiDocument) {
// return;
// }
// queryClient.setQueryData(
// getAiDocumentOptions({ documentSlug: documentSlug }).queryKey,
// {
// ...aiDocument,
// title: '',
// difficulty: '',
// modules: [],
// },
// );
// await generateDocument({
// term: aiDocument.keyword,
// difficulty: aiDocument.difficulty,
// slug: documentSlug,
// prompt,
// onDocumentChange: (document) => {
// queryClient.setQueryData(
// getAiDocumentOptions({ documentSlug: documentSlug }).queryKey,
// {
// ...aiDocument,
// title: aiDocument.title,
// difficulty: aiDocument.difficulty,
// content: document,
// },
// );
// },
// onLoadingChange: (isNewLoading) => {
// setIsRegenerating(isNewLoading);
// if (!isNewLoading) {
// // TODO: Update progress
// }
// },
// onError: setError,
// isForce: true,
// });
};
return <AIGuideContent html={aiGuide?.html || ''} />;
}

View File

@@ -1,148 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { BookOpen, Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import {
listUserAIGuidesOptions,
type ListUserAIGuidesQuery,
} from '../../queries/ai-guide';
import { queryClient } from '../../stores/query-client';
import { AITutorTallMessage } from '../AITutor/AITutorTallMessage';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { Pagination } from '../Pagination/Pagination';
import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
import { AIGuideCard } from '../AIGuide/AIGuideCard';
export function UserGuidesList() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const [pageState, setPageState] = useState<ListUserAIGuidesQuery>({
perPage: '21',
currPage: '1',
query: '',
});
const { data: userAiGuides, isFetching: isUserAiGuidesLoading } = useQuery(
listUserAIGuidesOptions(pageState),
queryClient,
);
useEffect(() => {
setIsInitialLoading(false);
}, [userAiGuides]);
const guides = userAiGuides?.data ?? [];
useEffect(() => {
const queryParams = getUrlParams();
setPageState({
...pageState,
currPage: queryParams?.p || '1',
query: queryParams?.q || '',
});
}, []);
useEffect(() => {
if (pageState?.currPage !== '1' || pageState?.query !== '') {
setUrlParams({
p: pageState?.currPage || '1',
q: pageState?.query || '',
});
} else {
deleteUrlParam('p');
deleteUrlParam('q');
}
}, [pageState]);
const isUserAuthenticated = isLoggedIn();
const isAnyLoading = isUserAiGuidesLoading || isInitialLoading;
return (
<>
{showUpgradePopup && (
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
disabled={isAnyLoading}
placeholder="Search guides..."
/>
{isAnyLoading && (
<p className="mb-4 flex flex-row items-center gap-2 text-sm text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading your guides...
</p>
)}
{!isAnyLoading && (
<>
<p className="mb-4 text-sm text-gray-500">
{isUserAuthenticated
? `You have generated ${userAiGuides?.totalCount} guides so far.`
: 'Sign up or login to generate your first guide. Takes 2s to do so.'}
</p>
{isUserAuthenticated && !isAnyLoading && guides.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3">
{guides.map((guide) => (
<AIGuideCard key={guide._id} guide={guide} />
))}
</div>
<Pagination
totalCount={userAiGuides?.totalCount || 0}
totalPages={userAiGuides?.totalPages || 0}
currPage={Number(userAiGuides?.currPage || 1)}
perPage={Number(userAiGuides?.perPage || 10)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{!isAnyLoading && guides.length === 0 && (
<AITutorTallMessage
title={
isUserAuthenticated ? 'No guides found' : 'Sign up or login'
}
subtitle={
isUserAuthenticated
? "You haven't generated any guides yet."
: 'Takes 2s to sign up and generate your first guide.'
}
icon={BookOpen}
buttonText={
isUserAuthenticated
? 'Create your first guide'
: 'Sign up or login'
}
onButtonClick={() => {
if (isUserAuthenticated) {
window.location.href = '/ai';
} else {
showLoginPopup();
}
}}
/>
)}
</>
)}
</>
);
}

View File

@@ -1,53 +0,0 @@
import { BookOpen, FileTextIcon, type LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
type LibraryTabsProps = {
activeTab: 'guides' | 'courses';
};
export function LibraryTabs(props: LibraryTabsProps) {
const { activeTab } = props;
return (
<div className="mb-6 flex gap-2 border-b border-gray-300">
<LibraryTabButton
isActive={activeTab === 'courses'}
icon={BookOpen}
label="Courses"
href="/ai/courses"
/>
<LibraryTabButton
isActive={activeTab === 'guides'}
icon={FileTextIcon}
label="Guides"
href="/ai/guides"
/>
</div>
);
}
type LibraryTabButtonProps = {
isActive: boolean;
icon: LucideIcon;
label: string;
href: string;
};
function LibraryTabButton(props: LibraryTabButtonProps) {
const { isActive, icon: Icon, label, href } = props;
return (
<a
href={href}
className={cn(
'flex items-center gap-1 rounded-t-md px-4 py-2 text-sm font-medium',
isActive
? 'bg-gray-300'
: 'bg-gray-100 transition-colors hover:bg-gray-200',
)}
>
<Icon className="h-4 w-4" />
{label}
</a>
);
}

View File

@@ -1,16 +0,0 @@
import { Loader2Icon } from 'lucide-react';
type LoadingChipProps = {
message?: string;
};
export function LoadingChip(props: LoadingChipProps) {
const { message = 'Please wait...' } = props;
return (
<div className="flex items-center gap-2 rounded-lg border border-gray-300 bg-white py-2 pr-4 pl-3 text-sm">
<Loader2Icon className="size-4 animate-spin text-gray-400" />
<span>{message}</span>
</div>
);
}

View File

@@ -2,7 +2,6 @@ import { type ReactNode, useRef } from 'react';
import { useOutsideClick } from '../hooks/use-outside-click';
import { useKeydown } from '../hooks/use-keydown';
import { cn } from '../lib/classname';
import { X } from 'lucide-react';
type ModalProps = {
onClose: () => void;
@@ -10,7 +9,6 @@ type ModalProps = {
overlayClassName?: string;
bodyClassName?: string;
wrapperClassName?: string;
hasCloseButton?: boolean;
};
export function Modal(props: ModalProps) {
@@ -20,7 +18,6 @@ export function Modal(props: ModalProps) {
bodyClassName,
wrapperClassName,
overlayClassName,
hasCloseButton = true,
} = props;
const popupBodyEl = useRef<HTMLDivElement>(null);
@@ -36,7 +33,7 @@ export function Modal(props: ModalProps) {
return (
<div
className={cn(
'fixed top-0 right-0 left-0 z-99 flex h-full items-center justify-center overflow-x-hidden overflow-y-auto bg-black/50',
'fixed left-0 right-0 top-0 z-99 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50',
overlayClassName,
)}
>
@@ -53,14 +50,6 @@ export function Modal(props: ModalProps) {
bodyClassName,
)}
>
{hasCloseButton && (
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-300 hover:text-gray-700"
>
<X className="h-5 w-5" />
</button>
)}
{children}
</div>
</div>

View File

@@ -8,9 +8,13 @@ import {
Users2,
Handshake,
} from 'lucide-react';
import { logout } from './navigation';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
import { useState } from 'react';
import { cn } from '../../lib/classname.ts';
import { NotificationIndicator } from './NotificationIndicator.tsx';
import { logout } from '../../lib/auth.ts';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
type AccountDropdownListProps = {
onCreateRoadmap: () => void;
@@ -39,7 +43,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
<li className="mb-1 px-1">
<button
className={cn(
'flex h-9 w-full items-center rounded-sm py-1 pr-2 pl-3 text-sm font-medium text-slate-100 hover:opacity-80',
'flex h-9 w-full items-center rounded-sm py-1 pl-3 pr-2 text-sm font-medium text-slate-100 hover:opacity-80',
isConfigLoading
? 'striped-loader-darker flex border-slate-800 opacity-70'
: 'border-slate-600 bg-slate-700',
@@ -47,7 +51,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
onClick={onOnboardingClick}
disabled={isConfigLoading}
>
<NotificationIndicator className="-top-0.5 -left-0.5" />
<NotificationIndicator className="-left-0.5 -top-0.5" />
{isConfigLoading ? (
<></>
@@ -66,7 +70,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
<li className="px-1">
<a
href="/account"
className="group flex items-center gap-2 rounded-sm py-2 pr-2 pl-3 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded-sm py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<User2 className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
Account
@@ -75,13 +79,13 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
<li className="px-1">
<a
href="/account/update-profile"
className="group flex items-center justify-between gap-2 rounded-sm py-2 pr-2 pl-3 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center justify-between gap-2 rounded-sm py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<span className="flex items-center gap-2">
<SquareUserRound className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
My Profile
</span>
<span className="rounded-xs bg-yellow-300 px-1 text-xs tracking-wide text-black uppercase">
<span className="rounded-xs bg-yellow-300 px-1 text-xs uppercase tracking-wide text-black">
New
</span>
</a>
@@ -89,7 +93,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
<li className="px-1">
<a
href="/account/friends"
className="group flex items-center gap-2 rounded-sm py-2 pr-2 pl-3 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded-sm py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Friends
@@ -100,7 +104,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
onClick={() => {
onCreateRoadmap();
}}
className="group flex w-full items-center gap-2 rounded-sm py-2 pr-2 pl-3 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex w-full items-center gap-2 rounded-sm py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Plus className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
New Roadmap
@@ -109,7 +113,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
<li className="border-b border-b-gray-700/60 px-1 pb-1">
<a
href="/account/roadmaps"
className="group flex items-center gap-2 rounded-sm py-2 pr-2 pl-3 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded-sm py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Map className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Roadmaps
@@ -117,7 +121,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
</li>
<li className="px-1 pt-1">
<button
className="group flex w-full items-center justify-between rounded-sm py-2 pr-2 pl-3 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex w-full items-center justify-between rounded-sm py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
onClick={() => setIsTeamsOpen(true)}
>
<span className="flex items-center gap-2.5">
@@ -129,7 +133,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
</li>
<li className="px-1">
<button
className="group flex w-full items-center gap-2 rounded-sm py-2 pr-2 pl-3 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex w-full items-center gap-2 rounded-sm py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
type="button"
onClick={logout}
>

Some files were not shown because too many files have changed in this diff Show More