initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.tagsContainer {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.tagsHeader {
display: flex;
align-items: center;
justify-content: space-between;
}
.tagsHeaderLabel {
font-weight: 500;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--text-primary);
}
.tagsInputRow {
display: flex;
gap: 0.5rem;
}
.tagsList {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tagChip {
display: flex;
align-items: center;
gap: 0.375rem;
border-radius: 9999px;
background-color: var(--brand-primary);
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-top: 0.375rem;
padding-bottom: 0.375rem;
font-size: 0.875rem;
line-height: 1.25rem;
color: white;
}
.tagRemoveButton {
display: flex;
height: 1rem;
width: 1rem;
align-items: center;
justify-content: center;
border-radius: 9999px;
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
cursor: pointer;
}
.tagRemoveButton:hover {
opacity: 0.7;
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import React from 'react';
import type {UseFormReturn} from 'react-hook-form';
import {MAX_FAVORITE_MEME_TAGS} from '~/Constants';
import {Input, Textarea} from '~/components/form/Input';
import {Button} from '~/components/uikit/Button/Button';
import styles from './MemeFormFields.module.css';
interface MemeFormFieldsProps {
form: UseFormReturn<{
name: string;
altText?: string;
tags: Array<string>;
}>;
disabled?: boolean;
}
export const MemeFormFields = observer(function MemeFormFields({form, disabled = false}: MemeFormFieldsProps) {
const {t} = useLingui();
const [tagInput, setTagInput] = React.useState('');
const [tags, setTags] = React.useState<Array<string>>(form.getValues('tags'));
React.useEffect(() => {
form.setValue('tags', tags, {shouldDirty: true});
}, [tags, form]);
const handleAddTag = React.useCallback(() => {
const trimmedTag = tagInput.trim();
if (
trimmedTag &&
trimmedTag.length >= 1 &&
trimmedTag.length <= 30 &&
tags.length < MAX_FAVORITE_MEME_TAGS &&
!tags.includes(trimmedTag)
) {
setTags([...tags, trimmedTag]);
setTagInput('');
}
}, [tagInput, tags]);
const handleRemoveTag = React.useCallback(
(tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove));
},
[tags],
);
const handleKeyDownTag = React.useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
},
[handleAddTag],
);
return (
<>
<Input
{...form.register('name', {
required: t`Name is required`,
maxLength: {
value: 100,
message: t`Name must be 100 characters or less`,
},
})}
type="text"
label={t`Name`}
placeholder={t`My awesome media`}
maxLength={100}
error={form.formState.errors.name?.message}
autoFocus={true}
required={true}
disabled={disabled}
/>
<Textarea
{...form.register('altText', {
maxLength: {
value: 500,
message: t`Alt text must be 500 characters or less`,
},
})}
label={t`Alt Text`}
placeholder={t`Describe the media`}
maxLength={500}
minRows={3}
maxRows={6}
error={form.formState.errors.altText?.message}
disabled={disabled}
/>
<div className={styles.tagsContainer}>
<div className={styles.tagsHeader}>
<span className={styles.tagsHeaderLabel}>
<Trans>
Tags ({tags.length}/{MAX_FAVORITE_MEME_TAGS})
</Trans>
</span>
</div>
<div className={styles.tagsInputRow}>
<Input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDownTag}
placeholder={t`Add a tag`}
maxLength={30}
disabled={tags.length >= MAX_FAVORITE_MEME_TAGS || disabled}
/>
<Button
onClick={handleAddTag}
disabled={!tagInput.trim() || tags.length >= MAX_FAVORITE_MEME_TAGS || disabled}
fitContent
>
<Trans>Add</Trans>
</Button>
</div>
{tags.length > 0 && (
<div className={styles.tagsList}>
{tags.map((tag) => (
<div key={tag} className={styles.tagChip}>
<span>{tag}</span>
{!disabled && (
<button type="button" onClick={() => handleRemoveTag(tag)} className={styles.tagRemoveButton}>
×
</button>
)}
</div>
))}
</div>
)}
</div>
</>
);
});