Before you go, would you consider becoming a sustainer to MPR News?
`),
bodySectionHTML: (`
Your monthly gift supports trusted journalism, music discovery, and community
conversation for all – no matter where you live or how you listen. From the
broadcast to the podcast, on-air and online, gifts from individuals power everything
you find at Minnesota Public Radio.
Would you consider becoming a sustainer by converting your one-time gift to a
monthly gift today?
`),
min: 100, // do not show if original gift is below this amount
max: 500, // do not show if original gift is above this amount
askAmount: (originalAmount) => {
// the input is the original amount
// the output is the ask amount, or false if the input does not map to an ask amount
if (originalAmount > 500.00) // $500+
return false; // don't show
if (originalAmount >= 400.00) // $400-$500
return 50.00;
if (originalAmount >= 300.00) // $300-$399
return 40.00;
if (originalAmount >= 200.00) // $200-$299
return 30.00;
if (originalAmount >= 100.00) // $100-$199
return 15.00;
if (originalAmount < 100.00) // $100-
return 10.00;
return false;
},
};
//
//
//
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//
//
//
window.NA.DonationForm.init({ makeTabbed: false }).then(async (donationFormApi) => {
//console.log("DonationForm:", donationFormApi);
console.log("Donation Form: %c READY %c", 'background-color: MediumSeaGreen; color: white; font-weight: bold;', 'background-color: unset; color: unset; font-weight: unset;');
console.log(donationFormApi);
const interrupter = await donationFormApi.DonationInterrupter.init({
min: DONATION_INTERRUPTER_OPTIONS.min,
max: DONATION_INTERRUPTER_OPTIONS.max,
askAmount: DONATION_INTERRUPTER_OPTIONS.askAmount,
popupHTML: {
headingHTML: DONATION_INTERRUPTER_OPTIONS.headingSectionHTML,
bodyHTML: DONATION_INTERRUPTER_OPTIONS.bodySectionHTML,
},
});
//console.log("DonationInterrupter", interrupter);
console.log("Donation Interrupter: %c READY %c", 'background-color: MediumSeaGreen; color: white; font-weight: bold;', 'background-color: unset; color: unset; font-weight: unset;');
console.log(interrupter);
//interrupter.show(); // debug: always show popup immediately when page loads
}).catch((error) => { console.log("Donation Interrupter: %c FAILED %c An error occured when initializing the API:", 'background-color: Tomato; color: white; font-weight: bold;', 'background-color: unset; color: Tomato; font-weight: unset;'), console.error(error) });
}catch(e) {console.error(e)}
return vwo_$('head')[0] && vwo_$('head')[0].lastChild;})("head")}}, R_940895_62_1_3_1:{ fn:function(log,nonce=''){return (function(x) {
if(!vwo_$.fn.vwoRevertHtml){
return;
};
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","editElement","STRONG:tm('Success! You’re subscribed!')"); vwo_debug*/(el=vwo_$(".vwo_tm_1742501918554 STRONG:tm('Success! You’re subscribed!')")).vwoRevertHtml(),(el=vwo_$("STRONG:tm('Success! You’re subscribed!')")).vwoRevertCss(),(el=vwo_$("STRONG:tm('Success! You’re subscribed!')")).vwoRevertCss();})(".vwo_tm_1742501918554 STRONG:tm('Success! You’re subscribed!')")}}, C_940895_62_1_3_2:{ fn:function(log,nonce=''){return (function(x) {var el,ctx=vwo_$(x);
/*vwo_debug log("remove",".field-item > p:nth-of-type(2)"); vwo_debug*/(el=vwo_$(".field-item > p:nth-of-type(2)")).vwoCss({display:"none !important"});})(".field-item > p:nth-of-type(2)")}}, C_940895_48_1_2_3:{ fn:function(log,nonce=''){return (function(x) {var el,ctx=vwo_$(x);
/*vwo_debug log("content",".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)"); vwo_debug*/(el=vwo_$(".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)")).replaceWith2("
You'll gain real-world insights into how economics impacts your daily life with this easy-to-follow online course. This crash course is based on the acclaimed textbook Economy, Society, and Public Policy by CORE Econ, tailored to help you grasp key concepts without feeling overwhelmed.
Whether you're new to economics or just want to deepen your understanding, this course covers the basics and connects them to today’s pressing issues—from inequality to public policy decisions.
Each week, you'll receive a reading guide that distills core principles, offers actionable takeaways, and explains how they affect the current world. While the full ebook enriches the experience, the guides alone provide a comprehensive understanding of fundamental economic ideas.
You’ll find this course especially useful and unique because…
It allows you to understand economics in action: Real-life examples and analysis of current events that show you economics at work.
There’s no prior knowledge required: Complex ideas are broken into simple, relatable explanations.
You can be flexible with your learning according to your lifestyle: Go at your own pace, with weekly guides that fit your schedule.
Are you ready to build a foundation in economics that empowers you to think critically about the world around you?
Get instant access today and keep an eye on your inbox for a confirmation email and your first lesson.
"),el=vwo_$(".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)");})(".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)")}}, R_940895_48_1_2_3:{ fn:function(log,nonce=''){return (function(x) {
if(!vwo_$.fn.vwoRevertHtml){
return;
};
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","content",".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)"); vwo_debug*/(el=vwo_$(".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)")).revertContentOp(),el=vwo_$(".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)");})(".stylingblock-content-margin-cell > table:nth-of-type(1) > tbody:nth-of-type(1) > tr:nth-of-type(1) > td:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1)")}}, C_940895_48_1_2_4:{ fn:function(log,nonce=''){return (function(x) {})(".author,.img-wrapper,.img-wrapper img,.author-copy")}}, C_940895_61_1_2_0:{ fn:function(log,nonce=''){return (function(x) {var el,ctx=vwo_$(x);
/*vwo_debug log("editElement","#tfa_75-L > b:nth-of-type(1)"); vwo_debug*/(el=vwo_$("#tfa_75-L > b:nth-of-type(1)")).html("1. How frequently do you listen to Minnesota Public Radio?");})("#tfa_75-L > b:nth-of-type(1)")}}, R_940895_61_1_2_0:{ fn:function(log,nonce=''){return (function(x) {
if(!vwo_$.fn.vwoRevertHtml){
return;
};
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","editElement","#tfa_75-L > b:nth-of-type(1)"); vwo_debug*/(el=vwo_$("#tfa_75-L > b:nth-of-type(1)")).vwoRevertHtml();})("#tfa_75-L > b:nth-of-type(1)")}}, C_940895_61_1_2_1:{ fn:function(log,nonce=''){return (function(x) {var el,ctx=vwo_$(x);
/*vwo_debug log("editElement","#tfa_87-L > b:nth-of-type(1)"); vwo_debug*/(el=vwo_$("#tfa_87-L > b:nth-of-type(1)")).html("2. Which MPR programs or segments do you listen to most often?");})("#tfa_87-L > b:nth-of-type(1)")}}, R_940895_61_1_2_1:{ fn:function(log,nonce=''){return (function(x) {
if(!vwo_$.fn.vwoRevertHtml){
return;
};
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","editElement","#tfa_87-L > b:nth-of-type(1)"); vwo_debug*/(el=vwo_$("#tfa_87-L > b:nth-of-type(1)")).vwoRevertHtml();})("#tfa_87-L > b:nth-of-type(1)")}}, C_940895_39_1_2_0:{ fn:function(log,nonce=''){return (function(x) {
try{
var _vwo_sel = vwo_$("`);
!vwo_$("head").find('#1740425171461').length && vwo_$('head').append(_vwo_sel);}catch(e) {console.error(e)}
try{}catch(e) {console.error(e)}
try{const getCurrentDate = (d = new Date()) => d.toISOString().split('T')[0];
function vwoCustomEvent (labelValue) {
window.VWO = window.VWO || [];
VWO.event = VWO.event || function () {VWO.push(["event"].concat([].slice.call(arguments)))};
VWO.event("customEvent", { "label": labelValue.toString() });
}
class RadioButtonComponent {
constructor (element) {
this.radio = element.querySelector('input[type="radio"]');
this.label = element.querySelector('label');
}
get value () {
let value = this.radio.value;
if (Number.isNaN(parseFloat(value)))
return value;
if (parseFloat(value) % 1 == 0)
return parseInt(value);
return parseFloat(value);
}
set value (newValue) {
this.radio.value = newValue;
}
get text () {
return this.label.textContent;
}
set text (newText) {
if (this.label.querySelector('.form-required')) {
const labelTextNode = [...this.label.childNodes].filter(({ nodeType }) => nodeType === Node.TEXT_NODE)[0];
labelTextNode.nodeValue = newText;
} else {
this.label.textContent = newText;
}
}
get checked () {
return this.radio.checked;
}
set checked (bool) {
this.click(),
bool === true && this.radio.checked === true;
}
click () {
this.label.click();
}
select () {
this.click();
}
addEventListener (eventType, callbackFunction) {
switch (eventType) {
case 'click':
this.label.addEventListener(eventType, callbackFunction);
case 'change':
default:
this.radio.addEventListener(eventType, callbackFunction);
}
}
}
class TextFieldComponent {
constructor (element) {
this.input = element.querySelector('input[type="text"]');
this.label = element.querySelector('label');
}
get value () {
return this.input.value;
}
set value (newValue) {
this.input.dispatchEvent(new Event('focus'));
this.input.value = newValue;
this.input.dispatchEvent(new Event('keyup'));
this.input.dispatchEvent(new Event('change'));
this.input.dispatchEvent(new Event('blur'));
}
get text () {
return this.input.placeholder;
}
set text (newText) {
this.input.placeholder = newText;
}
addEventListener (eventType, callbackFunction) {
this.input.addEventListener(eventType, callbackFunction);
}
}
class GiftArrayButton extends RadioButtonComponent {
constructor (element) {
super(element);
}
get amount () {
return this.value;
}
set amount (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) <= 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseInt(newAmount);
this.text = '$' + newAmount;
this.value = newAmount;
}
}
class GiftArrayOtherAmount extends TextFieldComponent {
constructor (element) {
super(element);
}
get amount () {
return parseFloat(this.value);
}
set amount (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) <= 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseFloat(newAmount);
this.value = newAmount;
this.input.dispatchEvent(new Event('updateSummary'));
}
}
class GiftArray extends Array {
constructor (items) {
if (!Array.isArray(items) && items.length === 0) {
throw new Error("GiftArray: Arugment 1 is not an instance of Array with a length greater than 0:" + items.join(', '));
}
if (items.every((item) => item instanceof GiftArrayButton || item instanceof GiftArrayOtherAmount)) {
if (items.find((item) => item instanceof GiftArrayOtherAmount)) {
let temp = items.find((item) => item instanceof GiftArrayOtherAmount);
items = items.filter((item) => item instanceof GiftArrayButton);
items.push(temp);
}
} else if (items.every((item) => item instanceof HTMLElement)) {
items = items.map((item) => item.matches(".webform-component-textfield") ? new GiftArrayOtherAmount(item) : new GiftArrayButton(item));
} else {
throw new Error("GiftArray: Arugment 1 is not of type HTMLElement, HTMLElement[], or GiftArrayButton|GiftArrayButton[]:" + items.join(', '));
}
super(...items);
this.Buttons = items.filter((item) => item instanceof GiftArrayButton);
this.OtherAmountInput = items.find((item) => item instanceof GiftArrayOtherAmount);
}
get amount () {
const activeButton = this.Buttons.find((item) => item.checked);
if (activeButton.value === "other") {
const otherButton = activeButton;
if (!otherButton) {
throw new Error("GiftArray.amount: Other Button was not defined.");
}
otherButton.click();
return this.OtherAmountInput.value;
} else {
return activeButton.value;
}
}
set amount (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) <= 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseFloat(newAmount);
const matchingButton = this.find((item) => item.value === newAmount);
if (matchingButton) {
matchingButton.click();
} else {
const otherButton = this.Buttons.find((item) => item.value === "other");
otherButton.click();
this.OtherAmountInput.amount = newAmount;
}
}
addEventListeners (eventType, callbackFunction, filter = undefined) {
if (filter && typeof filter === 'function') {
const filteredItems = this.filter((item) => filter.call(this, item));
filteredItems.forEach((item) => item.addEventListener(eventType, callbackFunction));
} else if (filter && typeof filter === 'string') {
if (filter.match(/buttons/gmi))
this.Buttons.forEach((item) => item.addEventListener(eventType, callbackFunction));
if (filter.match(/other/gmi))
this.OtherAmountInput.addEventListener(eventType, callbackFunction);
} else {
this.forEach((item) => item.addEventListener(eventType, callbackFunction));
}
}
}
class FrequencyButton extends RadioButtonComponent {
constructor (element) {
super(element);
}
get frequency () {
return this.text.match(/Monthly/gmi) ? "Monthly" : "One-Time";
}
set freqency (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) <= 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseInt(newAmount);
this.text = '$' + newAmount;
this.value = newAmount;
}
}
class FrequencyArray extends Array {
constructor (items) {
if (!Array.isArray(items) && items.length === 0) {
throw new Error("FrequencyArray: Arugment 1 is not an instance of Array with a length greater than 0:" + items.join(', '));
}
/*if (items.every((item) => item instanceof GiftArrayButton || item instanceof GiftArrayOtherAmount)) {
if (items.find((item) => item instanceof GiftArrayOtherAmount)) {
let temp = items.find((item) => item instanceof GiftArrayOtherAmount);
items = items.filter((item) => item instanceof GiftArrayButton);
items.push(temp);
}
} else*/ if (items.every((item) => item instanceof HTMLElement)) {
items = items.map((item) => item.matches(".webform-component-textfield") ? new GiftArrayOtherAmount(item) : new GiftArrayButton(item));
} else {
throw new Error("FrequencyArray: Arugment 1 is not of type HTMLElement or HTMLElement[]:" + items.join(', '));
}
super(...items);
this.Buttons = items.filter((item) => item instanceof GiftArrayButton);
}
get frequency () {
const activeButton = this.Buttons.find((item) => item.checked);
if (activeButton.value === "recurs")
return "monthly";
if (activeButton.value === "NO_RECURR")
return "one-time";
return activeButton.value;
}
set frequency (newFrequency) {
const reNewFrequencyValue = new RegExp(newFrequency, 'gmi');
const matchingButton = this.find((item) => item.value.match(reNewFrequencyValue) || item.text.match(reNewFrequencyValue));
matchingButton.click();
}
get recurring () {
return this.frequency === "monthly" ? true : false;
}
set recurring (bool) {
this.frequency = bool === true ? "monthly" : "one-time";
}
addEventListeners (eventType, callbackFunction, filter = undefined) {
if (filter && typeof filter === 'function') {
const filteredItems = this.filter((item) => filter.call(this, item));
filteredItems.forEach((item) => item.addEventListener(eventType, callbackFunction));
} else if (filter && typeof filter === 'string') {
if (filter.match(/buttons/gmi))
this.Buttons.forEach((item) => item.addEventListener(eventType, callbackFunction));
if (filter.match(/other/gmi))
this.OtherAmountInput.addEventListener(eventType, callbackFunction);
} else {
this.forEach((item) => item.addEventListener(eventType, callbackFunction));
}
}
}
//
const lockedProperty = { writable: false, configurable: false, enumerable: true };
function DonationFormAPI (elements, options = {}) {
const defaultOptions = {
min: 1.00,
max: 999999.99,
makeTabbed: false,
fakeSubmit: true,
overrideGiftArrayValues: false,
};
options = { ...defaultOptions, ...options };
//
const { frequencyRadios, submitButton, root } = elements;
const [ amountRadiosOnetime, amountRadiosMonthly ] = elements.amountRadios;
const oneTimeOtherAmountWrapper = amountRadiosOnetime.find((div) => !div.matches('.webform-component-textfield') || div.querySelector('input[type="text"]'));
const oneTimeRadioButtons = amountRadiosOnetime.filter((div) => div !== oneTimeOtherAmountWrapper);
const monthlyOtherAmountWrapper = amountRadiosMonthly.find((div) => !div.matches('.webform-component-textfield') || div.querySelector('input[type="text"]'));
const monthlyRadioButtons = amountRadiosMonthly.filter((div) => div !== monthlyOtherAmountWrapper);
const debug = {
log: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
info: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
warn: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
error: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
};
//
const api = new Object();
Object.defineProperty(api, 'root', {
value: root,
writable: false,
configurable: true,
enumerable: true,
});
Object.defineProperties(api, {
'FORM_MINIMUM': {
value: options.min || 0,
...lockedProperty
},
'FORM_MAXIMUM': {
value: options.max || Infinity,
...lockedProperty
},
});
Object.defineProperties(api, {
GiftArrays: {
value: {
"one-time": new GiftArray([ ...oneTimeRadioButtons, oneTimeOtherAmountWrapper ]),
"monthly": new GiftArray([ ...monthlyRadioButtons, monthlyOtherAmountWrapper ]),
},
writable: false,
configurable: true,
enumerable: true,
},
Frequencies: {
value: new FrequencyArray(frequencyRadios),
writable: false,
configurable: true,
enumerable: true,
},
SubmitButton: {
value: submitButton,
writable: false,
configurable: false,
enumerable: true,
}
});
Object.defineProperties(api, {
'getFrequency': {
value: async function () {
if (!this || this === null) throw new Error("validate: Unable to read API context.");
return new Promise((resolve, reject) => {
try {
resolve(this.Frequencies.frequency);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
'setFrequency': {
value: async function (frequency) {
if (!this || this === null) throw new Error("validate: Unable to read API context.");
return new Promise(async (resolve, reject) => {
try {
this.Frequencies.frequency = frequency;
if (await this.getFrequency() === frequency)
resolve(frequency);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
'getAmount': {
value: async function (frequency = undefined) {
if (!this || this === null) throw new Error("validate: Unable to read API context.");
return new Promise(async (resolve, reject) => {
try {
frequency = frequency || await this.getFrequency();
if (frequency && this.GiftArrays.hasOwnProperty(frequency)) {
const activeGiftArray = this.GiftArrays[frequency];
resolve(activeGiftArray.amount);
} else {
throw new Error("getAmount: Invalid frequency: " + frequency);
}
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
'setAmount': {
value: async function (amount, frequency = undefined) {
if (!this || this === null) throw new Error("validate: Unable to read API context.");
return new Promise(async (resolve, reject) => {
try {
const currentFrequency = await this.getFrequency();
if (!frequency) {
frequency = currentFrequency;
} else if (frequency !== currentFrequency) {
frequency = await this.setFrequency(frequency);
}
if (frequency && this.GiftArrays.hasOwnProperty(frequency)) {
const activeGiftArray = this.GiftArrays[frequency];
activeGiftArray.amount = amount;
} else {
throw new Error("setAmount: Invalid frequency: " + frequency);
}
if (await this.getAmount() === amount)
resolve(amount);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
'getRecurring': {
value: async function () {
if (!this || this === null) throw new Error("validate: Unable to read API context.");
return new Promise((resolve, reject) => {
try {
resolve(this.Frequencies.recurring);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
'setRecurring': {
value: async function (bool) {
if (!this || this === null) throw new Error("validate: Unable to read API context.");
return new Promise(async (resolve, reject) => {
try {
this.Frequencies.frequency = bool ? true : false;
if (await this.getRecurring() === bool)
resolve(bool);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
freqency: {
get () { return this.getFrequency() },
set (value) { this.setFrequency(value) },
enumerable: true, configurable: true,
},
amount: {
get () { return this.getAmount() },
set (value) { this.setAmount(value) },
enumerable: true, configurable: true,
},
recurring: {
get () { return this.getRecurring() },
set (value) { this.setRecurring(value) },
enumerable: true, configurable: true,
},
});
Object.defineProperties(api, {
'submit': {
value: async function (condition = this.validate||function(){return true}) {
//this.hooks['onBeforeSubmit'].forEach((callback) => callback.call(this));
let result;
const isAsyncFunction = (func) => func.constructor.name === "AsyncFunction";
if (Array.isArray(condition)) {
if (condition.every((c) => typeof c === 'function' && isAsyncFunction(c))) {
result = await Promise.all(condition.map(async (c) => await c.call(this)));
} else if (condition.every((c) => typeof c === 'function')) {
result = condition.every((c) => c.call(this));
} else if (condition.every((c) => c === true || c === false)) {
result = condition.every((c) => c);
}
} else if (typeof condition === 'function' && isAsyncFunction(condition)) {
result = await condition.call(this);
} else if (typeof condition === 'function') {
result = condition.call(this);
} else if (condition === true || condition === false) {
result = condition;
} else {
console.error("Unknown error.");
debugger;
}
//
if (result === true) {
if (window.NA.DonationForm.hasOwnProperty("DEBUG_MODE") && window.NA.DonationForm["DEBUG_MODE"] == true)
return console.log("Submit aborted (debug mode is enabled).");
this.SubmitButton.click(),
this.hooks['onSubmit'].forEach((callback) => callback.call(this));
//this.hooks['onAfterSubmit'].forEach((callback) => callback.call(this));
} else {
return console.log("Submit failed (conditions did not evaluate to true).");
}
}, ...lockedProperty
},
'interceptSubmit': {
value: function (handleInterceptedSubmit = () => { return new Promise((resolve) => resolve(undefined)) }) {
try {
window.NA.DonationForm.SubmitButtonCopy = window.NA.DonationForm.SubmitButtonCopy || createNewSubmitButton(window.NA.DonationForm.SubmitButton, { cloneOriginal: false, hideOriginal: true, observeOriginal: false });
window.NA.DonationForm.SubmitButtonCopy.addEventListener('click', async (event) => {
event.preventDefault(), event.stopPropagation();
const shouldFormSubmit = await handleInterceptedSubmit.call(this, event);
if (shouldFormSubmit) {
console.info("Submit allowed by initial interceptSubmit callback function resulting in a truthy evaluation.");
const formIsValid = await window.NA.DonationForm.validate();
if (!formIsValid) { // if submit allowed but there are known errors in the form
console.warn("Form has known errors. Attempting to submit to show errors then retrying.");
console.log("Submitting...");
window.NA.DonationForm.submit(true); // submit anyway to trigger the error to be shown
window.NA.DonationForm.SubmitButton.style.setProperty("display", "none"), debug.info("SubmitButton hidden."), // hide the original submit button again
window.NA.DonationForm.SubmitButtonCopy.style.setProperty("display", "none"), debug.info("SubmitButtonCopy hidden."); // hide the copy of the submit button again
window.NA.DonationForm.SubmitButtonCopy.style.removeProperty("display"), debug.info("SubmitButtonCopy unhidden."); // show the copy of the submit button
} else {
console.log("Submitting...");
window.NA.DonationForm.submit(true);
}
} else {
console.log("Submit prevented.");
console.info("Next submit will be allowed.");
window.NA.DonationForm.SubmitButton.style.removeProperty("display"), debug.info("SubmitButton unhidden."); // show the original submit button so that if something goes wrong the user can still click the submit button
window.NA.DonationForm.SubmitButtonCopy.style.setProperty("display", "none"), debug.info("SubmitButtonCopy hidden."); // hide the copy of the submit button that intercepts submit attempts so that there aren't two buttons
}
});
console.log("Submit intercept added.\nButton:", window.NA.DonationForm.SubmitButtonCopy);
} catch (error) {
console.error("Failed to add submit intercept:", error);
}
}, ...lockedProperty
},
'validate': {
value: async function (root = undefined) {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
root = root || this.root;
const flattenArray = (array) => array.reduce((flat, toFlatten) => flat.concat(Array.isArray(toFlatten) ? flattenArray(toFlatten) : toFlatten), []);
try {
const freqency = await this.getFrequency(),
amount = await this.getAmount();
if (!freqency || !amount)
return false;
if (amount < this.FORM_MINIMUM || amount > this.FORM_MAXIMUM)
return console.error("validate:", "Gift amount is invalid:", amount), false;
let requiredFields = Array.from(root.querySelectorAll('label:has(.form-required)'))
.map((label) => document.getElementById(label.htmlFor) || (label.nextElementSibling || label.previousElementSibling))
.filter((_) => !!_) // remove blanks
.filter((field) => {
if (field.name && field.name.includes('[payment_information]'))
return false;
return true;
})
.map((field) => {
if (field.matches("div"))
return [...field.querySelectorAll('input')];
return field;
})
requiredFields = flattenArray(requiredFields);
const valid = requiredFields.every((input) => {
const type = input.tagName.toLowerCase() === 'select' ? 'select' : input.type;
const { name, value, id } = input;
//console.log(type, name, value);
if (name === 'submitted[payment_information][payment_fields][credit][card_number]') {
if (value && value.length === 16)
return true;
return console.error("validate:", name+':', "CC is invalid."), false;
}
if (name === 'submitted[leadership_circle]')
return true;
if (name === 'submitted[donation][other_amount]' || name === 'submitted[donation][recurring_other_amount]')
if (amount)
return true;
switch (type) {
case 'email':
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(value))
return console.error("validate:", name+':', "Email address is invalid.\n", input, value), false;
return true;
case 'tel':
if (!value || value.length < 10)
return console.error("validate:", name+':', "Phone number is invalid.\n", input, value), false;
return true;
case 'select':
case 'radio':
case 'text':
if (!value || value.length === 0)
return console.error("validate:", name+':', "Field is invalid.\n", input, value), false;
return true;
default:
debug.log("default");
return true;
}
/*if (!value || value.length === 0)
return false;*/
});
return valid;
} catch (error) {
console.error(error);
return false;
}
}, ...lockedProperty
},
//'makeTabbed': { value: function(){} },
'DonationInterrupter': {
value: { init: initDonationInterrupter.bind(api) },
enumerable: true,
configurable: true,
writable: true,
}
});
initHooks(api, ['onFrequencyChange', 'onAmountChange', 'onTrySubmit', 'onSubmit']);
api.Frequencies.addEventListeners('change', (event) => {
if (event.target.checked) {
api.hooks['onFrequencyChange'].forEach((callback) => {
callback.call(api, event.target.value);
});
}
});
Object.entries(api.GiftArrays).forEach(([ key, GiftArray ]) => {
GiftArray.addEventListeners('change', (event) => {
if (event.target.checked) {
api.hooks['onAmountChange'].forEach((callback) => {
callback.call(api, event.target.value);
});
}
});
});
api.SubmitButton.addEventListener('click', (event) => {
api.hooks['onTrySubmit'].forEach((callback) => callback.call(api, event));
});
api.root.addEventListener('submit', (event) => {
api.hooks['onSubmit'].forEach((callback) => callback.call(api, event));
});
if (options.makeTabbed)
api.makeTabbed();
/*if (options.fakeSubmit)
window.NA.DonationForm.SubmitButtonCopy = window.NA.DonationForm.SubmitButtonCopy || createNewSubmitButton(window.NA.DonationForm.SubmitButton, { cloneOriginal: false, hideOriginal: true, observeOriginal: false });*/
return api;
}
function createNewSubmitButton (originalSubmitButton = window.NA.DonationForm.SubmitButton, options = {}) {
const defaultOptions = {
cloneOriginal: true,
hideOriginal: true,
observeOriginal: true,
};
options = { ...defaultOptions, ...options };
const newSubmitButton = document.createElement('button');
//newSubmitButton.id = "submit-button-copy";
newSubmitButton.classList.add("btn");
newSubmitButton.textContent = originalSubmitButton.value;
originalSubmitButton.after(newSubmitButton);
options.hideOriginal && originalSubmitButton.style.setProperty("display", "none");
return newSubmitButton;
}
function initHooks (api, hookNames = []) {
const hooks = Object.fromEntries(hookNames.map((hookName) => ([hookName, new Array()])));
Object.defineProperty(api, 'hooks', {
value: hooks,
...lockedProperty
});
}
function initDonationInterrupter (options = {}) {
const getExpId = () => {
let experiments = window._vwo_exp;
experiments = Object.entries(window._vwo_exp);
let id = experiments.find(([id, data]) => {
const name = data.name;
return name.match(/Donation Interrupter/);
})[1]?.id;
return id;
};
const getExpVariation = (id) => {
let experiment = window._vwo_exp[id];
return experiment.combination_chosen || experiment.combination_selected;
};
const defaultOptions = {
id: [ 'VWO', getExpId(), getExpVariation(getExpId()) ].join('-'),
tokenName: ("NA__MPR_DonationInterrupter:" + [ 'VWO', getExpId(), getExpVariation(getExpId()) ].join('-')),
min: 10,
max: 100,
askAmount: (originalAmount) => {
if (originalAmount > 500) // $500+
return false; // don't show
if (originalAmount >= 400) // $400-$500
return 50;
if (originalAmount >= 300) // $300-$399
return 40;
if (originalAmount >= 200) // $200-$299
return 30;
if (originalAmount >= 100) // $100-$199
return 15;
if (originalAmount < 100) // $100-
return 10;
return false;
},
askFrequency: (originalFrequency) => {
return "monthly";
},
popupHTML: {
headingHTML: (`
Lorem ipsum dolor sit amet
`),
bodyHTML: (`
Consectetur adipiscing elit. Sed nec egestas turpis, hendrerit semper nisl. Pellentesque auctor ipsum at
pharetra eleifend. Pellentesque a rhoncus turpis, ut tempus nibh. Donec vel dui hendrerit nisi imperdiet
tincidunt. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Duis efficitur dolor ut nisl blandit imperdiet. Nullam pretium est nunc, tincidunt viverra ligula dapibus.
Duis malesuada:
dui eu venenatis volutpat
urna libero posuere lectus
non tincidunt mauris ligula consectetur felis
lacinia hendrerit enim at molestie
Sed placerat fringilla consequat. Nullam eu pellentesque sem?
By submitting, you consent that you are at least 18 years of age and to receive information about MPR's or APMG entities' programs and offerings. The personally identifying information you provide will not be sold, shared, or used for purposes other than to communicate with you about MPR, APMG entities, and its sponsors. You may opt-out at any time clicking the unsubscribe link at the bottom of any email communication. View our Privacy Policy.
A memorial in Duluth, Minn., to Elmer Jackson, Isaac McGhie and Elias Clayton, who were lynched across the street from the site in 1920 on April 24, 2018.
Around the turn of the last century, many white people in the United States were eager to draw a thick, bold line separating the races. Sometimes they drew the line in blood. The Tuskegee Institute recorded nearly 5,000 lynchings of black people between 1880 and 1930. Historians say there were thousands more.
Most lynchings happened in Georgia, Mississippi and Alabama — in the Deep South. So it was a shock when the headline, "Duluth Mob Lynches Three Negroes," ran in papers from the Duluth News-Tribune to the New York Times. But the story quickly faded from the news, and most people in Duluth were happy to forget the murders. Two generations of Minnesotans grew up knowing little or nothing about the lynching in Duluth.
Turn Up Your Support
MPR News helps you turn down the noise and build shared understanding. Turn up your support for this public resource and keep trusted journalism accessible to all.
June 15 is the anniversary of the lynching. A few years ago, a citizens group began a campaign to build a memorial to the lynching victims in downtown Duluth. That memorial is now in place, and is the focus of ceremonies in downtown Duluth to remember the event on this anniversary. Duluth has regained its memory.
A mob lynches three black men
Duluth police station after damage by lynching mob in 1920.
Minnesota Historical Society
On a June night in 1920, hundreds of angry men and thousands of curious onlookers surrounded the downtown headquarters of the Duluth police department. The crowd might have reached 10,000. They wanted the handful of police officers inside to turn over their prisoners — a group of young, black circus workers. The police had arrested the men earlier that day. They accused some of the out-of-towners of raping a young, white woman at the circus grounds. Later investigations cast serious doubt on the rape charges, but the howling mob outside the police station had no doubts.
"This is where the mob broke in," says Michael Fedo, who wrote a book about the 1920 lynchings. "I think this was a Sears store or a hardware store. The mob came into this store — which is now the casino — and the proprietor gave them rope for the hangings and said it was on the house."
Standing in the heart of downtown Duluth, Fedo points across Superior St. to a handsome, three-story brownstone building full of offices. The word "POLICE" is still carved in the stone over the door.
Fedo says when the mob closed in on the police station, the city's public safety commissioner ordered the 12 officers inside to holster their guns. He didn't want anyone in the crowd to get hurt. A few officers came out onto the street, and tried to fight the mob back with their bare hands and a fire hose. But the crowd surged past them into the jail, with a roar that could be heard a mile away.
Cellblock in Duluth police station damaged by lynching mob in 1920.
Minnesota Historical Society
"Most of the cells were on the second floor, so they went in and broke into several of the cells."
While members of the mob sawed and smashed on the bars, some of the men inside the cells pleaded their innocence. Others prayed.
"The people in the mob believed that six had attacked the girl, so they tried to get six — they only managed to get into three of the cells. There were several people in the cells with the prisoners, asking questions, trying to find out in their minds who the six were among the more than a dozen who were in the cells," says Fedo.
"The people who were outside were saying, 'Just give us somebody,' and that first somebody was a young man named Isaac McGhie, who was just thrown from the cell to the hands of the mob who took him out front, brought him up the hill here one block, where he was the first one hanged," Fedo recalls.
Isaac McGhie was beaten and bloody when he got to this corner, right next to the Duluth Shrine Temple, which is still here.
Caption: Where hanging took place on Sixth Avenue East and First Street, Duluth Herald, June 16, 1920.
Minnesota Historical Society
"This is where they were brought to be hanged. I don't know why they would have been brought up the hill instead of down the hill. But it may have been because there was a young man perched on top of this pole, and they just assumed, 'He's already there, we'll take them up there, we'll have this kid tie a knot on the lamppost above the street, and take care of business that way,'" says Fedo.
A priest named William Powers pushed his way to the front of the crowd, and climbed part way up the lamppost. The priest managed to quiet the crowd for a few moments. He begged them to stop. But members of the mob pulled Rev. Powers down, and hoisted Isaac McGhie up.
Then the mob dragged Elmer Jackson and Elias Clayton out of the jail, and up the hill to the street light. When all three men were hanging, battered and dead, the crowd parted so a photographer could capture the scene.
"This was a significantly posed photo," says Fedo. "It took a couple of automobiles with lights to illuminate the scene so the photographer could get his picture taken."
In the center of the crisp, black and white photo, Elmer Jackson and Isaac McGhie hang from the street light, stripped to the waist, their necks impossibly stretched and twisted. Elias Clayton lies beneath their feet, tossed onto the sidewalk, to make it easier to frame the picture. Dozens of men lean into the picture facing the camera.
"What this looks like is the kind of photo you would see at a hunting lodge, where the guys had been out shooting bear, and they came back and they said, 'We got three.' You can see people on tip-toe. They've crowded into this shot. These are not people who are ashamed to be seen here. This is, 'I want to be in this picture.'"
Duluth lynching from the postcard photographed in June, 1920.
Minnesota Historical Society
"The one that quite stood out is the fellow who's to the left of the bodies who is beaming. He looks like he's very proud of what has transpired, and that is the face that really stands out to me," says Fedo.
Someone made postcards out of the photo, and sold them as souvenirs. Postcards of lynchings were fairly common. A recent book, Without Sanctuary, is a collection of photos and postcards from nearly 100 lynchings. It includes the picture from Duluth.
A lynching in northern Minnesota was big news. It made headlines across the country. It stayed in the local news for months during the criminal trials that followed. Juries in Duluth convicted three men of rioting. The longest sentence served was two years. No one was convicted of murder. But one of the black men who survived the attack on the jail was convicted of rape, in spite of compelling evidence he was innocent. He served four years in prison.
And then, the story of the lynching disappeared from the news.
The memories fade, except among blacks
In 1920, a few hundred black people lived in Duluth, clustered on the edge of town. Most of them came north because they'd heard there were jobs.
Ethel Ray Nance, left, on the Phyllis Wheatley intermediate diamond ball team in Minneapolis in 1926.
Minnesota Historical Society
Ethel Ray Nance grew up in Duluth. She says black people here mostly kept to themselves, and kept quiet. In the 1970s, Nance recorded her memories for the state Historical Society. She says her father tried to start a local chapter of the NAACP before the lynching.
"But the Negroes said they weren't interested, and that he was trying to segregate them. They said, 'We have no trouble here in Duluth so we don't need an NAACP branch.' But after this happened, he had no trouble," says Nance.
Roy Wilkins
Minnesota Historical Society
The lynching jarred black people across Minnesota into action. Roy Wilkins was a student at the University of Minnesota. He went on to become one of the country's most respected civil rights leaders, directing the NAACP during the 1960s and '70s. In his autobiography, Wilkins says news of the lynching was an awakening. He writes: "I lost my innocence on race once and for all."
Newspapers fell silent about the lynching. It didn't make the history books. But black people in Duluth never stopped telling the story — quietly — after church, or over the kitchen table.
Several generations of black parents handed the story down to their children as a rite of passage. That's what Eddie Nichols did. On the night of the lynching, Eddie Nichols had barricaded himself and some friends in their house. They thought the mob might come for them. Years later, Nichols told the story to his sons. One them, Charles, is now 77, but he remembers when his father sat him down with his brother for a talk.
"And he told us we had to live the life we could, but know there were things below the surface that were going to affect us," says Nichols. "From time to time he and my mother would both drop little precautions like that, because we thought everything was rosy, but they said it isn't. The veneer is awful thin."
Charles Nichols' boyhood in Duluth might have seemed rosy, but when he became a teenager in the 1940s, he was afraid to even walk in some Duluth neighborhoods. When Charles Nichols took the streetcar to Gary, where most black people lived, he says he felt a twinge of fear as the car passed through Morgan Park. The U.S. Steel plant in Morgan Park employed a small number of low-paid black workers, which angered some of the white workers. Charles Nichols says black parents in Duluth warned their children never to get off the streetcar in Morgan Park.
Aerial view of Morgan Park and the United States Steel Plant, Duluth in 1910
Minnesota Historical Society
"It was just an unwritten rule — you didn't do that. Unfortunately, the streetcar that went to Gary circled around through Morgan Park. My parents were always concerned that if something went wrong with the streetcar and we had to get off in Morgan Park, we might be in harm's way," says Nichols.
For decades, black families in Duluth handed the story down. When someone new moved to town, old-timers would pull them aside and tell them about the lynching. Samie McCurley's family moved to Duluth in 1950. When he was in high school, one of his friends showed him the postcard of the lynching.
"Me being a kid coming from Arkansas, those things like that did happen down south. I never, ever believed anything like that would happen up north here, so it was just real shocking."
And when Samie McCurley had children of his own, he told them the story.
"I told them it's something I never want them to forget. I have the picture and I gave it to them personally. I wanted them to be aware of what transpired in this town that they were born and were raised in."
A writer brings the story back to life
As decades passed, memories of the lynching faded, like an old photograph tucked away in a shoebox. But they never disappeared. Over the years, pieces of the story worked their way to the surface in surprising ways. Back in the 1940s, Sinclair Lewis moved to Duluth after he'd won the Nobel Prize for literature. Lewis worked on his novel, Kingsblood Royal, in Duluth, and he interviewed Eddie Nichols and other black people about their memories of the lynching. Lewis set his story in a fictional northern Minnesota city, an awful lot like Duluth. The book makes repeated references to lynching, and it bitterly attacks racism.
Twenty years after Lewis' novel, Bob Dylan recorded his song, Desolation Row. Dylan was born in Duluth. His father lived in a downtown apartment at the time of the lynching, and almost certainly knew about it. Bob Dylan's song contains several fragments of the lynching story — the circus, a riot squad, the waffling police commissioner.
Ten more years passed, and in the mid-1970s, a teacher in the Twin Cities named Michael Fedo decided to write a historical novel. He planned to set the story in the early part of the century in his hometown of Duluth. As Fedo put together an outline of the book, he remembered something his mother had mentioned years before, in passing. There'd been a lynching. He knew nothing more about it, but he thought it might add a dramatic twist to his novel.
Author Michael Fedo sits for a portrait inside his Coon Rapids, Minn., home on April 13, 2018.
Evan Frost | MPR News
"It was going to be a peripheral event that I was going to include in a chapter perhaps. I would just simply pick up the book that somebody must have written back in 1925 or so — only to discover there was no book," says Fedo. "The historical society had a folder that had a few clippings it, so I just started to poke around. After reading about it over the course of a week or two, I filled a spiral notebook with notes, and then I decided this is the story. This should have been written years ago, and I forgot about the novel and wrote this instead."
Fedo's book came out in 1979. He says the initial response was, "overwhelming indifference." He sold about 3,000 copies before the small publisher went bankrupt. But it did get some people talking. A few high school teachers started using excerpts in their history classes. Local newspapers sometimes ran stories on the anniversary of the lynching. And a few people got to wondering about the question Fedo raised in the last paragraph of the book.
Where were the bodies?
Dignifying the dead
On a sunny, spring morning, a few miles from downtown Duluth, Craig Grau climbs out of his van and goes walking among the graves at the Park Hill Cemetery. Only some of the graves have markers, because this section of the cemetery was reserved for poor people. But Grau has been here many times, and it only takes him a minute to find what he's looking for — three small granite slabs set low into the grass.
"They have the names, and they have the dates — the best they could tell on the birth dates. The death date they were sure of, which is 1920. And then the saying underneath each of them is, 'Deterred but not Defeated.'"
Grau doesn't know how the bodies of Elmer Jackson, Elias Clayton and Isaac McGhie ended up here.
Detail of the Duluth lynching from the postcard photographed in June, 1920.
Minnesota Historical Society
"It's clear they did end up here. If you look at the records of the cemetery it has the three names, and it lists under cause of death, 'Lynched.'"
For 70 years, the bodies lay here in unmarked graves. The cemetery caretaker knew they were here, but no one asked. Craig Grau found out they were here 10 years ago. The cemetery is connected with the Lutheran church Grau belongs to, so he figured the church should mark the graves. He urged his church and the local NAACP to work together raising money for markers.
On October 26, 1991, many Minnesotans were giddy because their hero, Kirby Puckett — a black man — hit an 11th-inning home run to win game six of the World Series. But here in the cemetery, a quiet crowd gathered to see the grave markers unveiled. Craig Grau says much of the credit for the markers goes to author Michael Fedo, who kept the memory of Clayton, Jackson, and McGhie alive.
"He put in the first edition of his book that the final indignity was that people didn't even know where they were buried," says Grau. "And I think he's right. I think that was an indignity. So they are now marked. My thinking has been, they were not treated as human beings in the last days of their lives — they should be treated as human beings in their death."
Duluth regains its memory
Since the graveyard ceremony, interest in the Duluth lynchings has grown — enough that the Minnesota Historical Society published a new edition of Michael Fedo's book last year. Back when the first edition of the book came out, the historical society didn't even write a review.
Several months ago, Fedo gave a lecture at the downtown library in Duluth. A snowstorm had shut down much of the city, and the weather service said to stay home. But about 70 people jammed into a conference room to listen to Fedo. He told them he never drew a crowd when the book first came out, but things have changed in the last 20 years. The men who posed with bodies in the photograph of the lynching have died.
"In 1979, many people who had some connection to this were still alive, and I think it was the kind of thing — people just simply not wanting to hear about it. In 1979 if we had done this, I would have been here with a couple members of my family," says Fedo.
Upstairs at the reference desk, the librarians say something similar. These days, more and more people want to know about the lynching. Head reference librarian David Ouse spins through a roll of microfilm.
"This is the Duluth News Tribune from June 16, 1920. 'Duluth Mob Hangs Negroes. Three Dragged From Jail.' Big headline."
Duluth News Tribune, June 16, 1920, "Duluth Mob Hangs Negroes."
Minnesota Historical Society
The film is scratched from use, and its cardboard box is falling apart from being handled.
"Ten or 15 years ago, there was the book, and occasionally I think students would do papers on it. But there's a lot more community interest now. We have some old newspaper clippings that, in the last few years, have gotten some pretty heavy use. And of course, a lot more copies of the book, new editions of the book," says Ouse.
Sometimes all 10 copies of the new edition are checked out, but Duluth Mayor Gary Doty recently found a copy on the shelf. Doty sat alone at a library table with the book and a stack of note cards. The mayor was reading up because he's agreed to help build a memorial to the lynching victims.
As mayor, Doty has spent a lot of city money cleaning up and redeveloping the city's downtown. He prides himself on bringing tourists and new businesses to Duluth. But when a citizens group proposed building a lynching memorial, he said 'No.' He thought it would be bad for Duluth's image. But then the mayor changed his mind.
"It's a terrible event, and one that I have mixed feelings about. It's one that I'd like to forget about in the respect that it happened in Duluth. But it's certainly something that we can't forget about because I think that something has to be done."
It's not just history
The people who persuaded the mayor to support a memorial for the lynching victims call themselves the Clayton-Jackson-McGhie Memorial Committee. Perry Kennedy is one of the oldest members of the group. When he joined the Army Air Corps after World War II, the military was still segregated. He served in an all-black unit. Like many people of his generation, he believes in attacking racism by getting people to face it, and talk about it. He wants to hear more people talk openly about the lynching.
Two children visit a memorial in Duluth, Minn., to three black men who were lynched across the street from the site in 1920 after being falsely accused of raping a white woman.
Dan Kraker | MPR News 2013
"Because to not discuss it leaves the ignorance there to fester. That is the reason for that type of thing happening — ignorance. And we understand now that if something happens, the best is to bring it out, examine it, and see what it's worth," says Kennedy.
Everyone on the memorial committee wants more discussion of the lynching, but some committee members worry about the way people discuss it. Melissa Taylor says people now talk about the lynching as a disturbing piece of Duluth past. But she says it's dangerous to think the lynching is just history.
"We don't actually hang men from lampposts, but all the undercurrent of emotion and hate and fear that drove this community to do that in 1920 still exists," says Taylor.
Detail of the mob at the Duluth lynching from the postcard photographed in June, 1920.
Minnesota Historical Society
When people look at the photograph of the lynching, they sometimes say, "How could that happen in Duluth? How could that happen at all?" Melissa Taylor has some idea. She's lived in Duluth all her life, but she says she feels like an outsider. She says people stare at her everywhere she goes. She's not willing to call it "hate," but it's hostility, as if there's an uneasy truce between the races.
For her, and for many other black people, there's no mystery about how it could happen — how a mob could drag three men from a jail, hang them in the middle of downtown, and snap a picture.
Postcard From A Lynching was written and produced by Minnesota Public Radio's Chris Julin and Stephanie Hemphill in 2001.
Thanks to Pat Maus at the Northeast Minnesota Historical Center.
News you can use in your inbox
When it comes to staying informed in Minnesota, our newsletters overdeliver. Sign-up now for headlines, breaking news, hometown stories, weather and much more. Delivered weekday mornings.