Finished, responsive
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@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 { Stats } from "@/components/Stats";
|
||||
import LetterDensity from "@/components/LetterDensity";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const config = {
|
||||
includeSpaces: true,
|
||||
@@ -16,6 +17,21 @@ export default function Home() {
|
||||
const [text, setText] = useState("");
|
||||
const [analysis, setAnalysis] = useState<any>(null);
|
||||
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(() => {
|
||||
const result: any = {};
|
||||
@@ -24,7 +40,10 @@ export default function Home() {
|
||||
result.charCount = includeSpaces ? inputText.length : inputText.replace(/\s/g, "").length;
|
||||
result.wordCount = (inputText.match(/[a-zA-ZäöüÄÖÜß]+/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);
|
||||
|
||||
const letters = inputText.toLowerCase().match(/[a-z]/g) || [];
|
||||
@@ -40,28 +59,51 @@ export default function Home() {
|
||||
}
|
||||
|
||||
setAnalysis(result);
|
||||
}, [text, includeSpaces]);
|
||||
}, [text, includeSpaces, useCustomLimit, charLimit]);
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl m-auto min-h-screen flex flex-col">
|
||||
<h1 className="text-6xl font-semibold text-center">
|
||||
<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 pt-16 pb-10">
|
||||
Analyze your Text<br />in real-time.
|
||||
</h1>
|
||||
|
||||
<Textarea
|
||||
className="h-72"
|
||||
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>
|
||||
<div className="flex flex-col md:flex-row space-x-2 pt-2 justify-between pb-8">
|
||||
<div className="w-auto gap-4 flex flex-col md:flex-row justify-items-center pb-8">
|
||||
<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">
|
||||
Exclude Spaces
|
||||
</label>
|
||||
</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>
|
||||
<p>Approx. reading time: {analysis?.readingTimeMinutes ?? 0} min</p>
|
||||
@@ -69,14 +111,14 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{analysis && (
|
||||
<>
|
||||
<div className="h-auto w-full">
|
||||
<Stats
|
||||
charCount={analysis.charCount}
|
||||
wordCount={analysis.wordCount}
|
||||
sentenceCount={analysis.sentenceCount}
|
||||
/>
|
||||
<LetterDensity density={analysis.letterDensity} />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {DarkModeToggle} from "@/components/DarkModeToggle";
|
||||
import Image from 'next/image'
|
||||
import NatterLogo from '@/app/natter-black.svg'
|
||||
|
||||
|
||||
export default function Header(){
|
||||
return(
|
||||
<header className="w-full h-16 flex justify-between p-4">
|
||||
<div>
|
||||
<h2 className="text-3xl">Logo</h2>
|
||||
<header className="w-full h-32 flex justify-between p-4 items-center">
|
||||
<div className="bg-white w-auto w-auto p-2 rounded-lg">
|
||||
|
||||
<Image className="h-8 w-auto" src={NatterLogo} alt={"Natter Logo"}></Image>
|
||||
</div>
|
||||
<div>
|
||||
<DarkModeToggle></DarkModeToggle>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
|
||||
type LetterDensityProps = {
|
||||
density: Record<string, number>;
|
||||
@@ -7,12 +8,14 @@ type LetterDensityProps = {
|
||||
|
||||
export default function LetterDensity({ density }: LetterDensityProps) {
|
||||
const sorted = Object.entries(density).sort((a, b) => b[1] - a[1]);
|
||||
const firstFive = sorted.slice(0, 5);
|
||||
const rest = sorted.slice(5);
|
||||
|
||||
return (
|
||||
<div className="h-auto w-full pt-8">
|
||||
<h2 className="text-2xl">Letter Density</h2>
|
||||
<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}>
|
||||
<p className="pr-4 text-lg uppercase">{letter}</p>
|
||||
<Progress className="h-4 flex-1" value={value * 100} />
|
||||
@@ -20,6 +23,24 @@ export default function LetterDensity({ density }: LetterDensityProps) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface StatsProps {
|
||||
|
||||
export function Stats({ charCount, wordCount, sentenceCount }: StatsProps) {
|
||||
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">
|
||||
<CardHeader className="relative">
|
||||
<CardTitle className="text-6xl font-semibold tabular-nums">{charCount}</CardTitle>
|
||||
|
||||
Reference in New Issue
Block a user