Share on twitter

This commit is contained in:
2025-02-22 17:01:42 +01:00
parent 1102b4d770
commit 574a738868
9 changed files with 971 additions and 18 deletions
+15
View File
@@ -3,6 +3,21 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Master touch typing with real code snippets from your favorite programming languages, powered by AI."
>
<meta
name="keywords"
content="touch typing, programming, code snippets, AI, artificial intelligence, wpm test, typing test, typing practice"
>
<meta property="og:title" content="Touch Programming">
<meta
property="og:description"
content="Master touch typing with real code snippets from your favorite programming languages, powered by AI."
>
<meta property="og:url" content=".">
<link rel="canonical" href=".">
<link rel="icon" sizes="192x192" href="android-chrome-192x192.png"> <link rel="icon" sizes="192x192" href="android-chrome-192x192.png">
<link rel="icon" sizes="512x512" href="android-chrome-512x512.png"> <link rel="icon" sizes="512x512" href="android-chrome-512x512.png">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
+923
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -35,6 +35,7 @@
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.22.0",
"vite": "^6.0.1", "vite": "^6.0.1",
"vite-plugin-mkcert": "^1.17.6", "vite-plugin-mkcert": "^1.17.6",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#dddddd" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>

After

Width:  |  Height:  |  Size: 402 B

+10 -2
View File
@@ -11,7 +11,7 @@
background: var(--black); background: var(--black);
} }
.share { .stats-container button {
all: unset; all: unset;
cursor: pointer; cursor: pointer;
@@ -21,9 +21,17 @@
border-radius: 0.75rem; border-radius: 0.75rem;
box-shadow: none; box-shadow: none;
padding: 1rem 2rem; padding: 0.5rem 1rem;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
display: flex;
align-items: center;
}
.stats-container svg {
width: 1.5rem;
height: 1.5rem;
} }
+7 -15
View File
@@ -2,7 +2,9 @@ import { useParams } from "react-router";
import { useTypingContext } from "contexts/typing"; import { useTypingContext } from "contexts/typing";
import { renderTimer } from "./utils"; import { renderTimer, shareOnTwitter } from "./utils";
import TwitterIcon from 'assets/icons/x.svg?react';
import './index.css'; import './index.css';
@@ -19,26 +21,16 @@ function Stats({ loaded }: StatsProps) {
} = useTypingContext(); } = useTypingContext();
const { lang } = useParams(); const { lang } = useParams();
async function share() { if (!loaded || !lang) return;
if (navigator.share) {
await navigator.share({
title: 'Touch Programming',
text: `I just Practiced The ${lang} Language on Touch Programming! I did ${score} WPM with ${accuracy}% accuracy. Try it out!`,
url: window.location.href,
});
}
}
if (!loaded) return;
return ( return (
<div className='stats-container'> <div className='stats-container'>
{renderTimer(timer)} {renderTimer(timer)}
<p>WPM: {Math.round(score)}</p> <p>WPM: {Math.round(score)}</p>
<p>Accuracy: {Math.round(accuracy)}%</p> <p>Accuracy: {Math.round(accuracy)}%</p>
{Boolean(navigator.share) && !startedTyping && score > 0 && ( {!startedTyping && score > 0 && (
<button className='share' onClick={share}> <button onClick={() => shareOnTwitter(lang, score, accuracy)}>
Share <TwitterIcon />
</button> </button>
)} )}
</div> </div>
+10
View File
@@ -6,3 +6,13 @@ export function renderTimer(timer: number) {
<p>Time: {leftValue < 10 && 0}{leftValue}:{rightValue < 10 && 0}{rightValue}</p> <p>Time: {leftValue < 10 && 0}{leftValue}:{rightValue < 10 && 0}{rightValue}</p>
); );
} }
export function shareOnTwitter(lang: string, score: number, accuracy: number) {
const tweetText = `I just practiced the ${lang} language on Touch Programming! I did ${Math.round(score)} WPM with ${Math.round(accuracy)}% accuracy. Try it out!`;
const url = window.location.href;
const encodedTweetText = encodeURIComponent(tweetText);
const encodedUrl = encodeURIComponent(url);
window.open(`https://x.com/intent/tweet?text=${encodedTweetText}&url=${encodedUrl}`);
}
+1
View File
@@ -1,4 +1,5 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_API_URL: string; readonly VITE_API_URL: string;
+3 -1
View File
@@ -2,12 +2,14 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react-swc';
import vitesTSConfigPaths from 'vite-tsconfig-paths'; import vitesTSConfigPaths from 'vite-tsconfig-paths';
import mkcert from 'vite-plugin-mkcert'; import mkcert from 'vite-plugin-mkcert';
import svgr from "vite-plugin-svgr";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
vitesTSConfigPaths(), vitesTSConfigPaths(),
mkcert() mkcert(),
svgr(),
], ],
server: { server: {
port: 3000, port: 3000,