diff --git a/apps/contrib/assets/HoverButton.jsx b/apps/contrib/assets/HoverButton.jsx
new file mode 100644
index 000000000..7af8d7ea8
--- /dev/null
+++ b/apps/contrib/assets/HoverButton.jsx
@@ -0,0 +1,37 @@
+import React, { useState, useEffect } from 'react'
+
+export const HoverButton = (props) => {
+ const { textMouseOn, textMouseOff, onClick } = props
+ const [buttonText, setButtonText] = useState(textMouseOff)
+ const [processing, setProcessing] = useState(false)
+
+ const handleClick = () => {
+ setProcessing(true)
+ onClick()
+ }
+
+ useEffect(() => {
+ setProcessing(false)
+ setButtonText(textMouseOff)
+ }, [textMouseOff])
+
+ return (
+
+ )
+}
diff --git a/apps/contrib/assets/__tests__/HoverButton.jest.jsx b/apps/contrib/assets/__tests__/HoverButton.jest.jsx
new file mode 100644
index 000000000..bcf2d5dfd
--- /dev/null
+++ b/apps/contrib/assets/__tests__/HoverButton.jest.jsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import { render, screen, fireEvent } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import { HoverButton } from '../HoverButton'
+
+test('Show button with normal text', () => {
+ render(
+
+ )
+ expect(screen.getByText('activated')).toBeInTheDocument()
+})
+
+test('Show button with hover text, via mouse', () => {
+ render(
+
+ )
+ fireEvent.mouseOver(screen.getByText('activated'))
+ expect(screen.getByText('deactivate')).toBeInTheDocument()
+ fireEvent.mouseOut(screen.getByText('deactivate'))
+ expect(screen.getByText('activated')).toBeInTheDocument()
+})
+
+test('Show button with hover text, via keyboard', () => {
+ render(
+
+ )
+ fireEvent.focus(screen.getByText('activated'))
+ expect(screen.getByText('deactivate')).toBeInTheDocument()
+ fireEvent.blur(screen.getByText('deactivate'))
+ expect(screen.getByText('activated')).toBeInTheDocument()
+})
+
+test('Show button with normal text and click on button', () => {
+ const onChangeFn = jest.fn()
+ render(
+
+ )
+ expect(screen.getByText('activated')).toBeInTheDocument()
+ fireEvent.click(screen.getByText('activated'))
+ expect(onChangeFn).toHaveBeenCalled()
+})
diff --git a/apps/contrib/dates.py b/apps/contrib/dates.py
new file mode 100644
index 000000000..39eca3252
--- /dev/null
+++ b/apps/contrib/dates.py
@@ -0,0 +1,8 @@
+from django.template import defaultfilters
+from django.utils import timezone
+
+
+def get_date_display(date):
+ local_date = timezone.localtime(date)
+ return '{}, {}'.format(defaultfilters.date(local_date),
+ defaultfilters.time(local_date))