Compare commits
2 Commits
67d2adb8bf
...
771248be35
Author | SHA1 | Date | |
---|---|---|---|
771248be35 | |||
563845dc68 |
@ -212,7 +212,14 @@ else
|
||||
fi`
|
||||
};
|
||||
|
||||
return samples[codeSample] || samples.javascript;
|
||||
type SampleLanguage = keyof typeof samples;
|
||||
|
||||
// Type guard to check if codeSample is a valid key of samples
|
||||
const isValidSample = (sample: string): sample is SampleLanguage => {
|
||||
return sample in samples;
|
||||
};
|
||||
|
||||
return isValidSample(codeSample) ? samples[codeSample] : samples.javascript;
|
||||
};
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
@ -289,7 +296,7 @@ fi`
|
||||
}}
|
||||
drag="x"
|
||||
dragConstraints={{ left: -50, right: 50 }}
|
||||
onDragEnd={(e, { offset, velocity }) => {
|
||||
onDragEnd={(e, { offset }) => {
|
||||
if (offset.x > 50) handleLike();
|
||||
else if (offset.x < -50) handleDislike();
|
||||
}}
|
||||
|
52
app/components/HelpDialog.tsx
Normal file
52
app/components/HelpDialog.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HelpDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
const HelpDialog: React.FC<HelpDialogProps> = ({ isOpen, onClose, isDarkMode }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl w-[90vw] max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold">How to Use TerminalTinder</h2>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<Image src={isDarkMode ? "/close-icon-dark.svg" : "/close-icon-light.svg"} alt="Close" width={24} height={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p>Welcome to TerminalTinder, the dating app that actually designed to be used over and over again.</p>
|
||||
|
||||
<h3 className="text-xl font-semibold">How it works:</h3>
|
||||
<ol className="list-decimal list-inside space-y-2">
|
||||
<li>You'll be presented with color schemes one at a time.</li>
|
||||
<li>Swipe right or click the heart icon to like a scheme.</li>
|
||||
<li>Swipe left or click the cross icon to dislike a scheme.</li>
|
||||
<li>The app learns from your preferences and generates new schemes based on what you like.</li>
|
||||
</ol>
|
||||
|
||||
<h3 className="text-xl font-semibold">Features:</h3>
|
||||
<ul className="list-disc list-inside space-y-2">
|
||||
<li>View a live preview of the color scheme applied to code.</li>
|
||||
<li>Change the programming language of the preview in the settings.</li>
|
||||
<li>Download color schemes in various formats (YAML, JSON, TOML, Xresources).</li>
|
||||
<li>View your liked and disliked schemes in the history.</li>
|
||||
</ul>
|
||||
|
||||
<p>The more you interact with TerminalTinder, the better it becomes at suggesting color schemes. All information is local, so refreshing the page refreshes learning.</p>
|
||||
<p>It's your internet, take it back.</p>
|
||||
<hr className="my-4 border-t border-gray-200 w-full" />
|
||||
<p>All credit for any non generated color schemes goes to their original creators. Color schemes are sourced from <a href="https://github.com/Mayccoll/Gogh" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-600">Gogh</a>.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpDialog;
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { ColorScheme } from '../utils/colorSchemes';
|
||||
import { generateYAML, generateJSON, generateXResources, generateTOML } from '../utils/exportFormats';
|
||||
import Image from 'next/image';
|
||||
@ -13,8 +13,6 @@ interface HistoryPopupProps {
|
||||
}
|
||||
|
||||
const HistoryPopup: React.FC<HistoryPopupProps> = ({ likedSchemes, dislikedSchemes, onClose, isDarkMode, outputFormat }) => {
|
||||
const [copiedColor, setCopiedColor] = useState<string | null>(null);
|
||||
|
||||
const handleDownload = (scheme: ColorScheme) => {
|
||||
let content: string;
|
||||
let fileExtension: string;
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
// Import the CodeSample type
|
||||
import { CodeSample } from '../utils/types';
|
||||
|
||||
interface SettingsProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@ -8,8 +11,8 @@ interface SettingsProps {
|
||||
onToggleDarkMode: () => void;
|
||||
outputFormat: string;
|
||||
setOutputFormat: (format: string) => void;
|
||||
codeSample: string;
|
||||
setCodeSample: (sample: string) => void;
|
||||
codeSample: CodeSample;
|
||||
setCodeSample: (sample: CodeSample) => void;
|
||||
saveSettings: boolean;
|
||||
setSaveSettings: (save: boolean) => void;
|
||||
}
|
||||
@ -91,7 +94,7 @@ const Settings: React.FC<SettingsProps> = ({
|
||||
<label className="block mb-2">Code Sample</label>
|
||||
<select
|
||||
value={codeSample}
|
||||
onChange={(e) => setCodeSample(e.target.value)}
|
||||
onChange={(e) => setCodeSample(e.target.value as CodeSample)}
|
||||
className="w-full p-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<option value="c">C</option>
|
||||
|
84
app/page.tsx
84
app/page.tsx
@ -5,8 +5,11 @@ import Image from 'next/image';
|
||||
import ColorSchemeCard from "./components/ColorSchemeCard";
|
||||
import HistoryPopup from "./components/HistoryPopup";
|
||||
import Settings from "./components/Settings";
|
||||
import HelpDialog from "./components/HelpDialog";
|
||||
import { ColorScheme, knownSchemes, generateRandomScheme, generateSchemeFromGeneticAlgorithm } from './utils/colorSchemes';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { CodeSample } from './utils/types';
|
||||
|
||||
|
||||
export default function Home() {
|
||||
const [schemes, setSchemes] = useState<ColorScheme[]>([]);
|
||||
@ -15,10 +18,24 @@ export default function Home() {
|
||||
const [dislikedSchemes, setDislikedSchemes] = useState<ColorScheme[]>([]);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
const [outputFormat, setOutputFormat] = useState('yaml');
|
||||
const [codeSample, setCodeSample] = useState('javascript');
|
||||
const [codeSample, setCodeSample] = useState<CodeSample>('javascript');
|
||||
const [saveSettings, setSaveSettings] = useState(false);
|
||||
|
||||
const generateNewSchemes = (count: number) => {
|
||||
const knownCount = Math.floor(count / 2);
|
||||
const generatedCount = count - knownCount;
|
||||
const newSchemes = [
|
||||
...knownSchemes.sort(() => 0.5 - Math.random()).slice(0, knownCount),
|
||||
...Array(generatedCount).fill(null).map(() =>
|
||||
likedSchemes.length > 0 ? generateSchemeFromGeneticAlgorithm(likedSchemes, dislikedSchemes) : generateRandomScheme()
|
||||
)
|
||||
];
|
||||
|
||||
setSchemes(prevSchemes => [...prevSchemes, ...newSchemes].sort(() => 0.5 - Math.random()));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
generateNewSchemes(8);
|
||||
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
@ -53,19 +70,6 @@ export default function Home() {
|
||||
localStorage.setItem('dislikedSchemes', JSON.stringify(dislikedSchemes));
|
||||
}, [dislikedSchemes]);
|
||||
|
||||
const generateNewSchemes = (count: number) => {
|
||||
const knownCount = Math.floor(count / 2);
|
||||
const generatedCount = count - knownCount;
|
||||
const newSchemes = [
|
||||
...knownSchemes.sort(() => 0.5 - Math.random()).slice(0, knownCount),
|
||||
...Array(generatedCount).fill(null).map(() =>
|
||||
likedSchemes.length > 0 ? generateSchemeFromGeneticAlgorithm(likedSchemes, dislikedSchemes) : generateRandomScheme()
|
||||
)
|
||||
];
|
||||
|
||||
setSchemes(prevSchemes => [...prevSchemes, ...newSchemes].sort(() => 0.5 - Math.random()));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (saveSettings) {
|
||||
const settings = JSON.stringify({ outputFormat, codeSample });
|
||||
@ -95,10 +99,6 @@ export default function Home() {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setIsDarkMode(prev => !prev);
|
||||
};
|
||||
|
||||
const toggleHistory = () => {
|
||||
setIsHistoryOpen(!isHistoryOpen);
|
||||
};
|
||||
@ -107,28 +107,33 @@ export default function Home() {
|
||||
setIsSettingsOpen(!isSettingsOpen);
|
||||
};
|
||||
|
||||
const getAllSchemes = () => {
|
||||
const allSchemes = [...likedSchemes, ...dislikedSchemes];
|
||||
const uniqueSchemes = allSchemes.filter((scheme, index, self) =>
|
||||
index === self.findIndex((t) => t.name === scheme.name)
|
||||
);
|
||||
return uniqueSchemes.reverse(); // Most recent first
|
||||
const toggleHelp = () => {
|
||||
setIsHelpOpen(!isHelpOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-screen overflow-hidden font-[family-name:var(--font-geist-sans)] dark:bg-gray-900 dark:text-white transition-colors duration-300">
|
||||
<header className="absolute top-4 left-4 z-20">
|
||||
<Image src="/app-icon.svg" alt="App Icon" width={32} height={32} />
|
||||
<header className="absolute top-2 left-2 right-2 flex justify-between items-start z-20">
|
||||
<div className="flex items-center">
|
||||
<Image src="/app-icon.svg" alt="App Icon" width={32} height={32} className="mr-2" />
|
||||
<div>
|
||||
<h1 className="text-lg font-bold">TerminalTinder</h1>
|
||||
<p className="text-xs">Fall in love with your next color scheme</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button onClick={toggleHistory}>
|
||||
<Image src={isDarkMode ? "/history-icon-dark.svg" : "/history-icon-light.svg"} alt="History" width={24} height={24} />
|
||||
</button>
|
||||
<button onClick={toggleSettings}>
|
||||
<Image src={isDarkMode ? "/settings-icon-dark.svg" : "/settings-icon-light.svg"} alt="Settings" width={24} height={24} />
|
||||
</button>
|
||||
<button onClick={toggleHelp}>
|
||||
<Image src={isDarkMode ? "/help-icon-dark.svg" : "/help-icon-light.svg"} alt="Help" width={24} height={24} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="absolute top-4 right-4 z-20 flex space-x-2">
|
||||
<button onClick={toggleHistory}>
|
||||
<Image src={isDarkMode ? "/history-icon-dark.svg" : "/history-icon-light.svg"} alt="History" width={24} height={24} />
|
||||
</button>
|
||||
<button onClick={toggleSettings}>
|
||||
<Image src={isDarkMode ? "/settings-icon-dark.svg" : "/settings-icon-light.svg"} alt="Settings" width={24} height={24} />
|
||||
</button>
|
||||
</div>
|
||||
<main className="flex flex-col items-center justify-center h-screen">
|
||||
<main className="flex flex-col items-center justify-center h-screen pt-16 sm:pt-20">
|
||||
<AnimatePresence>
|
||||
{schemes.slice(0, 3).map((scheme, index) => (
|
||||
<ColorSchemeCard
|
||||
@ -162,11 +167,18 @@ export default function Home() {
|
||||
outputFormat={outputFormat}
|
||||
setOutputFormat={setOutputFormat}
|
||||
codeSample={codeSample}
|
||||
setCodeSample={setCodeSample}
|
||||
setCodeSample={(sample: CodeSample) => setCodeSample(sample)}
|
||||
saveSettings={saveSettings}
|
||||
setSaveSettings={setSaveSettings}
|
||||
/>
|
||||
)}
|
||||
{isHelpOpen && (
|
||||
<HelpDialog
|
||||
isOpen={isHelpOpen}
|
||||
onClose={toggleHelp}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -132,20 +132,20 @@ function generateSchemeFromGeneticAlgorithm(likedSchemes: ColorScheme[], dislike
|
||||
const newScheme: ColorScheme = JSON.parse(JSON.stringify(parentScheme)); // Deep copy
|
||||
|
||||
// Mutate colors
|
||||
Object.keys(newScheme.colors).forEach((colorGroup: keyof typeof newScheme.colors) => {
|
||||
Object.keys(newScheme.colors[colorGroup]).forEach((colorName: string) => {
|
||||
(Object.keys(newScheme.colors) as Array<keyof typeof newScheme.colors>).forEach((colorGroup) => {
|
||||
Object.keys(newScheme.colors[colorGroup]).forEach((colorName) => {
|
||||
if (Math.random() < 0.3) { // 30% chance of mutation
|
||||
(newScheme.colors[colorGroup] as any)[colorName] = mutateColor((newScheme.colors[colorGroup] as any)[colorName]);
|
||||
(newScheme.colors[colorGroup] as Record<string, string>)[colorName] = mutateColor((newScheme.colors[colorGroup] as Record<string, string>)[colorName]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Avoid similarities with disliked schemes
|
||||
dislikedSchemes.forEach(dislikedScheme => {
|
||||
Object.keys(newScheme.colors).forEach((colorGroup: keyof typeof newScheme.colors) => {
|
||||
Object.keys(newScheme.colors[colorGroup]).forEach((colorName: string) => {
|
||||
if ((newScheme.colors[colorGroup] as any)[colorName] === (dislikedScheme.colors[colorGroup] as any)[colorName]) {
|
||||
(newScheme.colors[colorGroup] as any)[colorName] = mutateColor((newScheme.colors[colorGroup] as any)[colorName]);
|
||||
(Object.keys(newScheme.colors) as Array<keyof typeof newScheme.colors>).forEach((colorGroup) => {
|
||||
Object.keys(newScheme.colors[colorGroup]).forEach((colorName) => {
|
||||
if ((newScheme.colors[colorGroup] as Record<string, string>)[colorName] === (dislikedScheme.colors[colorGroup] as Record<string, string>)[colorName]) {
|
||||
(newScheme.colors[colorGroup] as Record<string, string>)[colorName] = mutateColor((newScheme.colors[colorGroup] as Record<string, string>)[colorName]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
1
app/utils/types.ts
Normal file
1
app/utils/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type CodeSample = 'c' | 'python' | 'rust' | 'go' | 'javascript' | 'java' | 'bash';
|
7
public/help-icon-dark.svg
Normal file
7
public/help-icon-dark.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M11.967 12.75C12.967 11.75 13.967 11.3546 13.967 10.25C13.967 9.14543 13.0716 8.25 11.967 8.25C11.0351 8.25 10.252 8.88739 10.03 9.75M11.967 15.75H11.977M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 791 B |
7
public/help-icon-light.svg
Normal file
7
public/help-icon-light.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M11.967 12.75C12.967 11.75 13.967 11.3546 13.967 10.25C13.967 9.14543 13.0716 8.25 11.967 8.25C11.0351 8.25 10.252 8.88739 10.03 9.75M11.967 15.75H11.977M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 791 B |
Loading…
x
Reference in New Issue
Block a user