├── .eslintrc ├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── lib.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── _document.jsx └── index.js ├── public └── favicon.ico ├── styles └── globals.css └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: push 4 | 5 | jobs: 6 | run-linters: 7 | name: Run linters 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Check out Git repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | 19 | - name: Install Node.js dependencies 20 | run: yarn 21 | 22 | - name: Run linters 23 | uses: wearerequired/lint-action@v1 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | # Enable linters 27 | prettier: true 28 | commit_message: "style: fix code style issues with ${linter}" 29 | auto_fix: true 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "printWidth": 90, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "semi": false, 7 | "singleQuote": false, 8 | "trailingComma": "none", 9 | "bracketSpacing": false, 10 | "jsxBracketSameLine": true, 11 | "arrowParens": "always", 12 | "requirePragma": false, 13 | "insertPragma": false, 14 | "proseWrap": "always" 15 | 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Jason Antwi-Appah 2 | 3 | This Source Code Form is subject to the terms of the Mozilla Public 4 | License, v. 2.0. If a copy of the MPL was not distributed with this 5 | file, You can obtain one at http://mozilla.org/MPL/2.0/. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wordcounter 2 | 3 | Next.js app to display the word count of given text. I probably could've done this in 4 | React or just vanilla JS but I wanted to get familiar with Next.js. I'm using Geist UI for 5 | most of the styling and icons, and it's deployed on Vercel. I learnt a lot more about 6 | regex, React state, and Next.js from this project ;) 7 | 8 | To do: 9 | 10 | - [x] Make it work hehe 11 | - [ ] Add Export to GitHub gist 12 | - [x] Add Export to .txt file 13 | 14 | Try it out at [wc.jasonaa.me](https://wc.jasonaa.me)! 15 | 16 | ![A screenshot of the site](https://f000.backblazeb2.com/file/jasonaa-static/img/wordcounter.png) 17 | 18 | --- 19 | 20 | This is a [Next.js](https://nextjs.org/) project bootstrapped with 21 | [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 22 | 23 | ## Getting Started 24 | 25 | First, run the development server: 26 | 27 | ```bash 28 | npm run dev 29 | # or 30 | yarn dev 31 | # yarn is best ;) 32 | ``` 33 | 34 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 35 | 36 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you 37 | edit the file. 38 | 39 | ## Learn More 40 | 41 | To learn more about Next.js, take a look at the following resources: 42 | 43 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and 44 | API. 45 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 46 | 47 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - 48 | your feedback and contributions are welcome! 49 | 50 | ## Deploy on Vercel 51 | 52 | The easiest way to deploy your Next.js app is to use the 53 | [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) 54 | from the creators of Next.js. 55 | 56 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for 57 | more details. 58 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | export function getTheme() { 2 | if (typeof Storage !== "undefined") { 3 | // can use window.localStorage, so use the set value if it exists 4 | if (getPreferredTheme() === "dark") return "dark" 5 | } 6 | return "light" 7 | } 8 | 9 | export function savePreferredTheme(theme) { 10 | if (typeof Storage !== "undefined") { 11 | localStorage.setItem("preferredTheme", theme) 12 | } 13 | } 14 | 15 | export function getPreferredTheme() { 16 | if (typeof Storage !== "undefined") { 17 | // can use window.localStorage, so use the set value if it exists 18 | if (localStorage.getItem("preferredTheme") === "dark") return "dark" 19 | } 20 | return "light" 21 | } 22 | 23 | export function saveStats(stats) { 24 | if (typeof Storage !== "undefined") { 25 | localStorage.setItem("stats", JSON.stringify(stats)) 26 | } 27 | } 28 | 29 | export function getTextFromStorage() { 30 | if (typeof Storage !== "undefined") { 31 | return localStorage.getItem("text") 32 | } else { 33 | return "" 34 | } 35 | } 36 | 37 | export function getStatsFromStorage() { 38 | if (typeof Storage !== "undefined") { 39 | return JSON.parse(localStorage.getItem("stats")) 40 | } else { 41 | return { 42 | chars: 0, 43 | words: 0, 44 | sentences: 0 45 | } 46 | } 47 | } 48 | 49 | export function saveText(text) { 50 | if (typeof Storage !== "undefined") { 51 | localStorage.setItem("text", text) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NEXT_PUBLIC_META_IMG: 4 | "https://f000.backblazeb2.com/file/jasonaa-static/img/wordcounter.png" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordcounter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@geist-ui/react": "^2.1.0-canary.2", 13 | "@geist-ui/react-icons": "^1.0.1", 14 | "inter-ui": "^3.19.2", 15 | "next": "^12.1.0", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^7.16.0", 21 | "eslint-config-next": "^11.0.1", 22 | "eslint-config-standard": "^16.0.2", 23 | "eslint-plugin-import": "^2.22.1", 24 | "eslint-plugin-node": "^11.1.0", 25 | "eslint-plugin-promise": "^4.2.1", 26 | "eslint-plugin-react": "^7.21.5", 27 | "prettier": "^2.3.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import {GeistProvider, CssBaseline} from "@geist-ui/react" 2 | import {useState, useEffect} from "react" 3 | import "../styles/globals.css" 4 | import "inter-ui/inter.css" 5 | import {Page, Text, Link, Button} from "@geist-ui/react" 6 | import {Github} from "@geist-ui/react-icons" 7 | import {getTheme, savePreferredTheme} from "../lib" 8 | function MyApp({Component, pageProps}) { 9 | const [themeType, setThemeType] = useState("dark") 10 | const [display, setDisplay] = useState(false) 11 | const switchThemes = () => { 12 | const newTheme = themeType === "dark" ? "light" : "dark" 13 | setThemeType(newTheme) 14 | savePreferredTheme(newTheme) 15 | } 16 | useEffect(() => { 17 | setDisplay(true) 18 | setThemeType(getTheme()) 19 | }, []) 20 | 21 | return ( 22 | 23 | 24 | {display ? ( 25 | 30 | ) : ( 31 | 43 | )} 44 | 45 | ) 46 | } 47 | 48 | export default MyApp 49 | -------------------------------------------------------------------------------- /pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import Document, {Html, Head, Main, NextScript} from "next/document" 2 | import {CssBaseline} from "@geist-ui/react" 3 | 4 | class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const initialProps = await Document.getInitialProps(ctx) 7 | const styles = CssBaseline.flush() 8 | 9 | return { 10 | ...initialProps, 11 | styles: ( 12 | <> 13 | {initialProps.styles} 14 | {styles} 15 | 16 | ) 17 | } 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | ) 30 | } 31 | } 32 | 33 | export default MyDocument 34 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | // lol this whole thing probably could be done in react but ssr go brrr 2 | 3 | import Head from "next/head" 4 | import {useEffect, useState} from "react" 5 | import { 6 | Page, 7 | Textarea, 8 | Text, 9 | Link, 10 | Grid, 11 | Spacer, 12 | Button, 13 | useToasts, 14 | useClipboard, 15 | Toggle, 16 | Col, 17 | User, 18 | Note, 19 | Row 20 | } from "@geist-ui/react" 21 | import {Copy, Download, Github} from "@geist-ui/react-icons" 22 | 23 | const center = {textAlign: "center"} 24 | import { 25 | getTheme, 26 | saveStats, 27 | getTextFromStorage, 28 | getStatsFromStorage, 29 | saveText 30 | } from "../lib" 31 | export default function Home({currentTheme, themeToggle}) { 32 | const [, setToast] = useToasts() 33 | const {copy} = useClipboard() 34 | useEffect(() => { 35 | const newText = getTextFromStorage() 36 | if (newText) setText(newText) 37 | 38 | const newStats = getStatsFromStorage() 39 | if (newStats) setStats(newStats) 40 | else if (!newStats && newText) { 41 | const newStats = { 42 | chars: newText.length, 43 | // i love/hate regex but its kinda cool when it works. 44 | // s/o https://regexr.com/ 45 | words: ((newText.split(/([\S])+/) || []).length - 1) / 2, 46 | sentences: (newText.split(/(!+|\?+|\.+)/).length - 1) / 2 47 | } 48 | setStats(newStats) 49 | saveStats(newStats) 50 | } 51 | }, []) 52 | 53 | const [text, setText] = useState("") 54 | const [textStats, setStats] = useState({ 55 | chars: 0, 56 | words: 0, 57 | sentences: 0 58 | }) 59 | function onChange(e) { 60 | const temp = e.target.value 61 | setText(temp) 62 | saveText(temp) 63 | 64 | // const tmp = temp.split(/([A-z])+/) || [] 65 | const tmp = temp.split(/([\S])+/) || [] 66 | const newStats = { 67 | chars: temp.length, 68 | // i love/hate regex but its kinda cool when it works. 69 | // s/o https://regexr.com/ 70 | words: (tmp.length - 1) / 2, 71 | sentences: (temp.split(/(!+|\?+|\.+)/).length - 1) / 2 72 | } 73 | setStats(newStats) 74 | saveStats(newStats) 75 | } 76 | 77 | return ( 78 | 79 | 80 | Word Counter 81 | 82 | 83 | 87 | 88 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 105 | 109 | 113 | 114 | 115 | 116 | 117 | Word Count. 118 | 119 | 120 | Enter your text below and see your current word count! Don't 121 | worry about saving - text will auto-save to your browser's 122 | storage after every edit. 123 | 124 | 125 | 126 |