Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo

/data
/data
/dev/data
69 changes: 69 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Contributing

Vad kul att du vill hjälpa till och utveckla nollk.it! Alla förbättringar leder
till en bättre upplevelse för både Nollan och NollKIT, så tack!

## Information om projektet

Projektet använder teknologierna i listan nedanför. Du behöver inte vara bekant
med alla för att göra ändringar men det gör det självklart lättare desto mer du
kan! Det skadar aldrig att försöka och du kan alltid fråga om hjälp.

- [Next.js](https://nextjs.org/)
- [TypeScript](https://www.typescriptlang.org/)
- [Node.js](https://nodejs.org/)
- [Tailwind](https://tailwindcss.com/)
- [Prisma](https://www.prisma.io/)
- [Docker](https://www.docker.com/) ([Docker Compose](https://docs.docker.com/compose/))
- [MongoDB](https://www.mongodb.com/)

## För att utveckla

Installera [Docker Compose](https://docs.docker.com/compose/install) och
[Node.js](https://nodejs.org/en/download) om du inte redan har det.

### Starta databasen

```console
docker compose --file dev/docker-compose.yml up -d
```

> [!WARNING]
> Om du får ett felmeddelande, [kontrollera att Docker körs](https://docs.docker.com/engine/daemon/troubleshoot/).

### Förbered webbsidan

1. Skapa filen `.env` med följande innehåll:

```env
DATABASE_URL=mongodb://localhost:27017/db
PASSWORD=123
```

2. Kör dessa kommandon:

```console
npm install -D
npx prisma generate
node data.js
```

> [!NOTE]
> `npm install -D` installerar alla Node dependencies, inklusive dependencies för utveckling.
> `npx prisma generate` genererar koden för Prisma klienten.
> `node data.js` skapar startdata i databasen.

### Starta webbsidan

```console
npm run dev
```

> [!TIP]
> Sidan uppdateras automatiskt när du ändrar källkoden.

### Stäng av databasen

```compose
docker compose --file dev/docker-compose.yml down
```
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,3 @@ Döp filerna till `år.pdf` eller `år.ttf`.

För att starta projektet bygg docker nånting nånting
Prisma generate? Gör det lätt att förstå helt enkelt.



## För att utveckla
Först kör npm i, sen npx prisma generate om prisma skulle gnälla.

Hur gör man med databasen?
Docker?

11 changes: 8 additions & 3 deletions components/Countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ export default function Countdown({ criticalDates }: { criticalDates: string[] }
seconds: number,
}

const [timeLeft, setTimeLeft] = useState<TimeLeft>({
const defaultTimeLeft: TimeLeft = {
days: 100,
hours: 0,
minutes: 0,
seconds: 0,
})
}

const [timeLeft, setTimeLeft] = useState<TimeLeft>(defaultTimeLeft)

const ctx = useContext(YearContext)

Expand All @@ -26,7 +28,10 @@ export default function Countdown({ criticalDates }: { criticalDates: string[] }
function getTimeLeft(year: string): TimeLeft {
const todaysDate = new Date()

const msDelta = new Date(findCriticalDate(criticalDates, year)).getTime() - todaysDate.getTime()
const criticalDate = findCriticalDate(criticalDates, year)
if (criticalDate === "")
return defaultTimeLeft
const msDelta = new Date(criticalDate).getTime() - todaysDate.getTime()

const daysLeft = Math.floor(msDelta / (86_400_000))
const hoursLeft = Math.floor((msDelta % 86_400_000) / 3_600_000)
Expand Down
10 changes: 7 additions & 3 deletions components/ImageWithFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ export default function ImageWithFallback(props: ImageWithFallbackProps) {

return useFallback ?
// If fallbacksrc is not set, don't render anything
props.fallbacksrc ? (
<Image {...props} src={props.fallbacksrc} />
(
props.fallbacksrc &&
typeof props.fallbacksrc === 'string' &&
props.fallbacksrc.trim() !== ''
) ? (
<Image {...props} unoptimized src={props.fallbacksrc} />
) : (
null
)
: (
<Image {...props} onError={() => setUseFallback(true)}/>
<Image {...props} unoptimized onError={() => setUseFallback(true)}/>
)
}
15 changes: 12 additions & 3 deletions components/ManagementDisplays/CommitteeManagementDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import TextInput from "../admin/TextInput"
import Accordion from "../admin/Accordion"

interface CommitteeManagementDisplayProps {
committee: CommitteeWithMembers
committee?: CommitteeWithMembers
}

export default function CommitteeManagementDisplay({ committee }: CommitteeManagementDisplayProps) {
if (!committee) {
return <p>Skapa en kommittée för att komma igång.</p>
}

const [members, setMembers] = useState(committee.members)
let newMembers = [...members]
Expand Down Expand Up @@ -155,15 +158,21 @@ export default function CommitteeManagementDisplay({ committee }: CommitteeManag
"Content-Type": "application/json",
},
body: JSON.stringify({ year: committee.year }),
}).then(() => {
}).then(() =>
fetch("/api/admin/committee/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(committeeWithMembers),
})
}).then(() => alert("Sparat " + committee.year + " till databasen!"))
).then(res => {
if (res.status === 200) {
alert("Sparat " + committee.year + " till databasen!")
} else {
alert("Något gick fel")
}
})
}}>
Spara till databasen
</Button>
Expand Down
175 changes: 135 additions & 40 deletions components/ManagementDisplays/TimelineManagementDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ interface TimelineManagementDisplayProps {

export default function TextManagementDisplay(props: TimelineManagementDisplayProps) {

const [selectedYear, setSelectedYear] = useState(props.committees.sort((a, b) => b.year - a.year)[0].year)
const [selectedYear, setSelectedYear] = useState(props.committees.sort((a, b) => b.year - a.year)[0]?.year ?? new Date().getFullYear())

const [shownEvents, setShownEvents] = useState(props.timelineEvents.filter(event => event.year === selectedYear.toString()).sort((a, b) => a.date > b.date ? 1 : -1))

const getCategoryColor = (categoryId: string) => {
return props.categories.find(category => category.title === categoryId)?.color ?? props.categories[0].color
const [shownCategories, setShownCategories] = useState<Category[]>(props.categories)

const [newCategory, setNewCategory] = useState<Category>({ title: "", color: "", })

const getCategoryColor = (categoryId: string): string | undefined => {
return shownCategories.find(category => category.title === categoryId)?.color ?? shownCategories[0]?.color
}

return <>
Expand All @@ -32,7 +36,10 @@ export default function TextManagementDisplay(props: TimelineManagementDisplayPr
}
</select>

<div className="flex flex-col gap-8 pb-8">
<section className="mb-8">
<h2 className="text-2xl mb-4">Tidslinje</h2>

<div className="flex flex-col gap-8 pb-8">
{
shownEvents.map((event, index) => {
return (
Expand Down Expand Up @@ -85,49 +92,140 @@ export default function TextManagementDisplay(props: TimelineManagementDisplayPr
)
})
}
</div>

<div className="flex gap-4">
<select id="category-select" className="p-1 rounded-md" style={{ backgroundColor: props.categories[0].color }} onChange={
e => {
e.target.style.backgroundColor = getCategoryColor(e.target.value)
}
}>
{
props.categories.map(category => {
return <option style={{ backgroundColor: category.color }} key={category.title} value={category.title}>
{category.title}
</option>
})
}
</select>

<Button action={() => {
setShownEvents([...shownEvents, {
id: "tempid",
categoryId: (document.getElementById("category-select") as HTMLSelectElement).value,
date: "",
text: "",
link: "",
year: selectedYear.toString()
}])
{
shownCategories.length === 0
? <p>Inga kategorier tillagda. Lägg till en kategori nedan.</p>
: <>
<div className="flex gap-4">
<select id="category-select" className="p-1 rounded-md" style={{ backgroundColor: shownCategories[0]?.color ?? "" }} onChange={
e => {
e.target.style.backgroundColor = getCategoryColor(e.target.value) ?? ""
}
}>
{
shownCategories.map(category => {
return <option style={{ backgroundColor: category.color }} key={category.title} value={category.title}>
{category.title}
</option>
})
}
</select>

<Button action={() => {
setShownEvents([...shownEvents, {
id: "tempid",
categoryId: (document.getElementById("category-select") as HTMLSelectElement).value,
date: "",
text: "",
link: "",
year: selectedYear.toString()
}])
}
}>
Lägg till
</Button>
</div>

<div className="mt-8">
<Button color="bg-green-500" action={() => {
fetch('/api/admin/timeline/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
events: shownEvents,
year: selectedYear
}
)
}).then(res => {
if (res.status === 200) {
alert("Sparat!")
} else {
alert("Något gick fel")
}
})
}}>
Spara
</Button>
</div>
</>
}
</section>

<section className="mb-8">
<h2 className="text-2xl mb-6">Kategorier</h2>
<div className="flex flex-col gap-4 pb-4">
{
shownCategories.map((category, index) => {
return (
<div key={category.title} className="bg-black/50 p-5">

<div className="flex items-start justify-between">
<div className="p-1 px-2" style={{ backgroundColor: getCategoryColor(category.title) }}>{category.title}</div>
<Button color="bg-red-500" action={() => {
const usedEvent = props.timelineEvents.find(event => event.categoryId === category.title)
?? shownEvents.find(event => event.categoryId === category.title)
if (usedEvent) {
alert(`Kan inte ta bort kategori, används år ${usedEvent.year} med text "${usedEvent.text}"`)
return
}

const newShownCategories = [...shownCategories]
newShownCategories.splice(index, 1)
setShownCategories(newShownCategories)
}}>
Ta bort
</Button>
</div>

<TextInput placeholder="Färg" setValue={
inputValue => {
const newShownCategories = [...shownCategories]
newShownCategories[index].color = inputValue
setShownCategories(newShownCategories)
}
}>
{category.color}
</TextInput>
</div>
)
})
}
}>
Lägg till
</Button>
</div>

<div>

<span className="flex flex-row items-center gap-4">
<TextInput
placeholder="Titel på kategori"
setValue={inputValue => {
const changedCategory = { ...newCategory, title: inputValue }
setNewCategory(changedCategory)
}}
>
{newCategory.title}
</TextInput>

<Button action={async () => {
setShownCategories([...shownCategories, newCategory])
setNewCategory({...newCategory, title: ""})
}}>
Lägg till
</Button>
</span>

<div className="mt-8">
<Button color="bg-green-500" action={() => {
fetch('/api/admin/timeline/update', {
fetch('/api/admin/category/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
events: shownEvents,
year: selectedYear
categories: shownCategories
}
)
}).then(res => {
Expand All @@ -140,10 +238,7 @@ export default function TextManagementDisplay(props: TimelineManagementDisplayPr
}}>
Spara
</Button>

</div>

</div >

</section>
</>
}
File renamed without changes.
Loading