Finished, responsive
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
|
|||||||
24
src/app/natter-black.svg
Normal file
24
src/app/natter-black.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 479.47 75">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #231f20;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_1-2" data-name="Layer_1">
|
||||||
|
<g>
|
||||||
|
<polygon class="cls-1" points="0 75 37.5 75 37.5 37.5 0 0 0 75"/>
|
||||||
|
<polygon class="cls-1" points="37.5 0 37.5 37.5 75 75 75 0 37.5 0"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M87.46,73.48V1.38h9.99l44.08,59.02-1.65.93c-.48-2.81-.88-6.08-1.18-9.79s-.53-7.79-.67-12.26c-.14-4.46-.21-9.2-.21-14.21V1.38h11.12v72.1h-10.2l-43.36-56.86,1.13-1.44c.48,5.22.86,9.49,1.13,12.82.27,3.33.48,5.99.62,7.98.14,1.99.22,3.55.26,4.69.03,1.13.05,2.08.05,2.83v29.97h-11.12Z"/>
|
||||||
|
<path class="cls-1" d="M161.62,73.48L189.53,1.38h11.33l27.71,72.1h-11.85l-16.48-43.47c-.21-.48-.6-1.65-1.18-3.5-.58-1.85-1.25-3.91-2.01-6.18-.76-2.27-1.44-4.34-2.06-6.23-.62-1.89-1.03-3.11-1.24-3.66l2.37-.1c-.41,1.17-.91,2.64-1.49,4.43-.58,1.79-1.2,3.67-1.85,5.67-.65,1.99-1.29,3.86-1.91,5.61-.62,1.75-1.13,3.21-1.54,4.38l-16.38,43.05h-11.33ZM173.98,55.56l4.12-10.71h32.75l4.74,10.71h-41.61Z"/>
|
||||||
|
<path class="cls-1" d="M254.93,73.48V12.09h-20.19V1.38h52.12v10.71h-20.81v61.39h-11.12Z"/>
|
||||||
|
<path class="cls-1" d="M314.26,73.48V12.09h-20.19V1.38h52.12v10.71h-20.81v61.39h-11.12Z"/>
|
||||||
|
<path class="cls-1" d="M359.89,73.48V1.38h45.42v10.71h-34.3v50.68h34.3v10.71h-45.42ZM364.94,41.86v-10.71h34.71v10.71h-34.71Z"/>
|
||||||
|
<path class="cls-1" d="M423.13,73.48V1.38h30.69c4.12,0,7.91,1.01,11.38,3.04,3.47,2.03,6.23,4.77,8.29,8.24,2.06,3.47,3.09,7.36,3.09,11.69,0,3.91-1.03,7.55-3.09,10.92-2.06,3.37-4.81,6.06-8.24,8.08-3.43,2.03-7.25,3.04-11.43,3.04h-19.57v27.09h-11.12ZM434.26,35.68h20.6c1.99,0,3.78-.5,5.36-1.49,1.58-.99,2.82-2.37,3.71-4.12.89-1.75,1.34-3.69,1.34-5.82,0-2.4-.57-4.51-1.7-6.33-1.13-1.82-2.71-3.25-4.74-4.27-2.03-1.03-4.31-1.54-6.85-1.54h-17.72v23.59ZM465.98,73.48l-18.33-32.55,11.43-2.58,20.39,35.23-13.49-.1Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -5,6 +5,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Stats } from "@/components/Stats";
|
import { Stats } from "@/components/Stats";
|
||||||
import LetterDensity from "@/components/LetterDensity";
|
import LetterDensity from "@/components/LetterDensity";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
includeSpaces: true,
|
includeSpaces: true,
|
||||||
@@ -16,6 +17,21 @@ export default function Home() {
|
|||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [analysis, setAnalysis] = useState<any>(null);
|
const [analysis, setAnalysis] = useState<any>(null);
|
||||||
const [includeSpaces, setIncludeSpaces] = useState(true);
|
const [includeSpaces, setIncludeSpaces] = useState(true);
|
||||||
|
const [useCustomLimit, setUseCustomLimit] = useState(false);
|
||||||
|
const [charLimit, setCharLimit] = useState(config.charLimit);
|
||||||
|
|
||||||
|
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newText = e.target.value;
|
||||||
|
const limitToUse = useCustomLimit ? charLimit : config.charLimit;
|
||||||
|
|
||||||
|
const currentLength = includeSpaces
|
||||||
|
? newText.length
|
||||||
|
: newText.replace(/\s/g, "").length;
|
||||||
|
|
||||||
|
if (currentLength <= limitToUse) {
|
||||||
|
setText(newText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const result: any = {};
|
const result: any = {};
|
||||||
@@ -24,7 +40,10 @@ export default function Home() {
|
|||||||
result.charCount = includeSpaces ? inputText.length : inputText.replace(/\s/g, "").length;
|
result.charCount = includeSpaces ? inputText.length : inputText.replace(/\s/g, "").length;
|
||||||
result.wordCount = (inputText.match(/[a-zA-ZäöüÄÖÜß]+/g) || []).length;
|
result.wordCount = (inputText.match(/[a-zA-ZäöüÄÖÜß]+/g) || []).length;
|
||||||
result.sentenceCount = (inputText.match(/[\w\s][.?!](?=\s|$)/g) || []).length;
|
result.sentenceCount = (inputText.match(/[\w\s][.?!](?=\s|$)/g) || []).length;
|
||||||
result.exceedsLimit = result.charCount > config.charLimit;
|
|
||||||
|
const limitToUse = useCustomLimit ? charLimit : config.charLimit;
|
||||||
|
result.exceedsLimit = result.charCount > limitToUse;
|
||||||
|
|
||||||
result.readingTimeMinutes = Math.ceil(result.wordCount / 200);
|
result.readingTimeMinutes = Math.ceil(result.wordCount / 200);
|
||||||
|
|
||||||
const letters = inputText.toLowerCase().match(/[a-z]/g) || [];
|
const letters = inputText.toLowerCase().match(/[a-z]/g) || [];
|
||||||
@@ -40,28 +59,51 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAnalysis(result);
|
setAnalysis(result);
|
||||||
}, [text, includeSpaces]);
|
}, [text, includeSpaces, useCustomLimit, charLimit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl m-auto min-h-screen flex flex-col">
|
<div className="max-w-6xl m-auto min-h-screen flex flex-col p-4 md:px-8 lg:p-0">
|
||||||
<h1 className="text-6xl font-semibold text-center">
|
<h1 className="text-6xl font-semibold text-center pt-16 pb-10">
|
||||||
Analyze your Text<br />in real-time.
|
Analyze your Text<br />in real-time.
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
className="h-72"
|
className="h-72"
|
||||||
placeholder="Start typing here...(or paste your text)"
|
placeholder="Start typing here...(or paste your text)"
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={handleTextChange}
|
||||||
|
value={text}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row space-x-2 pt-2 justify-between">
|
<div className="flex flex-col md:flex-row space-x-2 pt-2 justify-between pb-8">
|
||||||
<div>
|
<div className="w-auto gap-4 flex flex-col md:flex-row justify-items-center pb-8">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox id="spaces" checked={includeSpaces} onCheckedChange={setIncludeSpaces} />
|
<Checkbox
|
||||||
|
id="spaces"
|
||||||
|
checked={includeSpaces}
|
||||||
|
onCheckedChange={(checked) => setIncludeSpaces(!!checked)}
|
||||||
|
/>
|
||||||
<label htmlFor="spaces" className="text-sm font-medium leading-none">
|
<label htmlFor="spaces" className="text-sm font-medium leading-none">
|
||||||
Exclude Spaces
|
Exclude Spaces
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 ">
|
||||||
|
<Checkbox
|
||||||
|
id="characterLimit"
|
||||||
|
checked={useCustomLimit}
|
||||||
|
onCheckedChange={(checked) => setUseCustomLimit(!!checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="characterLimit" className="text-sm font-medium leading-none">
|
||||||
|
Set Character Limit
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={charLimit}
|
||||||
|
onChange={(e) => setCharLimit(Number(e.target.value))}
|
||||||
|
disabled={!useCustomLimit}
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Approx. reading time: {analysis?.readingTimeMinutes ?? 0} min</p>
|
<p>Approx. reading time: {analysis?.readingTimeMinutes ?? 0} min</p>
|
||||||
@@ -69,14 +111,14 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{analysis && (
|
{analysis && (
|
||||||
<>
|
<div className="h-auto w-full">
|
||||||
<Stats
|
<Stats
|
||||||
charCount={analysis.charCount}
|
charCount={analysis.charCount}
|
||||||
wordCount={analysis.wordCount}
|
wordCount={analysis.wordCount}
|
||||||
sentenceCount={analysis.sentenceCount}
|
sentenceCount={analysis.sentenceCount}
|
||||||
/>
|
/>
|
||||||
<LetterDensity density={analysis.letterDensity} />
|
<LetterDensity density={analysis.letterDensity} />
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {DarkModeToggle} from "@/components/DarkModeToggle";
|
import {DarkModeToggle} from "@/components/DarkModeToggle";
|
||||||
|
import Image from 'next/image'
|
||||||
|
import NatterLogo from '@/app/natter-black.svg'
|
||||||
|
|
||||||
|
|
||||||
export default function Header(){
|
export default function Header(){
|
||||||
return(
|
return(
|
||||||
<header className="w-full h-16 flex justify-between p-4">
|
<header className="w-full h-32 flex justify-between p-4 items-center">
|
||||||
<div>
|
<div className="bg-white w-auto w-auto p-2 rounded-lg">
|
||||||
<h2 className="text-3xl">Logo</h2>
|
|
||||||
|
<Image className="h-8 w-auto" src={NatterLogo} alt={"Natter Logo"}></Image>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DarkModeToggle></DarkModeToggle>
|
<DarkModeToggle></DarkModeToggle>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||||
|
|
||||||
type LetterDensityProps = {
|
type LetterDensityProps = {
|
||||||
density: Record<string, number>;
|
density: Record<string, number>;
|
||||||
@@ -7,12 +8,14 @@ type LetterDensityProps = {
|
|||||||
|
|
||||||
export default function LetterDensity({ density }: LetterDensityProps) {
|
export default function LetterDensity({ density }: LetterDensityProps) {
|
||||||
const sorted = Object.entries(density).sort((a, b) => b[1] - a[1]);
|
const sorted = Object.entries(density).sort((a, b) => b[1] - a[1]);
|
||||||
|
const firstFive = sorted.slice(0, 5);
|
||||||
|
const rest = sorted.slice(5);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-auto w-full pt-8">
|
<div className="h-auto w-full pt-8">
|
||||||
<h2 className="text-2xl">Letter Density</h2>
|
<h2 className="text-2xl">Letter Density</h2>
|
||||||
<div className="flex flex-col py-4 gap-2">
|
<div className="flex flex-col py-4 gap-2">
|
||||||
{sorted.map(([letter, value]) => (
|
{firstFive.map(([letter, value]) => (
|
||||||
<div className="flex flex-row items-center" key={letter}>
|
<div className="flex flex-row items-center" key={letter}>
|
||||||
<p className="pr-4 text-lg uppercase">{letter}</p>
|
<p className="pr-4 text-lg uppercase">{letter}</p>
|
||||||
<Progress className="h-4 flex-1" value={value * 100} />
|
<Progress className="h-4 flex-1" value={value * 100} />
|
||||||
@@ -20,6 +23,24 @@ export default function LetterDensity({ density }: LetterDensityProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{rest.length > 0 && (
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
<AccordionItem value="item-1">
|
||||||
|
<AccordionTrigger className="text-xl">See more</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="flex flex-col py-4 gap-2">
|
||||||
|
{rest.map(([letter, value]) => (
|
||||||
|
<div className="flex flex-row items-center" key={letter}>
|
||||||
|
<p className="pr-4 text-lg uppercase">{letter}</p>
|
||||||
|
<Progress className="h-4 flex-1" value={value * 100} />
|
||||||
|
<p className="pl-4">{(value * 100).toFixed(2)}%</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface StatsProps {
|
|||||||
|
|
||||||
export function Stats({ charCount, wordCount, sentenceCount }: StatsProps) {
|
export function Stats({ charCount, wordCount, sentenceCount }: StatsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row space-x-4 gap-4 h-32">
|
<div className="flex flex-col md:flex-row space-x-4 gap-4 w-full">
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader className="relative">
|
<CardHeader className="relative">
|
||||||
<CardTitle className="text-6xl font-semibold tabular-nums">{charCount}</CardTitle>
|
<CardTitle className="text-6xl font-semibold tabular-nums">{charCount}</CardTitle>
|
||||||
|
|||||||
Reference in New Issue
Block a user